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

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Merge commit '882d8795c6ff65c02f2657e596391748d1b6b7f5'
authormartin f. krafft <madduck@madduck.net>
Fri, 20 Oct 2023 12:39:05 +0000 (14:39 +0200)
committermartin f. krafft <madduck@madduck.net>
Fri, 20 Oct 2023 12:39:05 +0000 (14:39 +0200)
317 files changed:
.vim/bundle/black/.coveragerc [deleted file]
.vim/bundle/black/.flake8
.vim/bundle/black/.git_archival.txt [new file with mode: 0644]
.vim/bundle/black/.gitattributes [new file with mode: 0644]
.vim/bundle/black/.github/ISSUE_TEMPLATE/bug_report.md
.vim/bundle/black/.github/ISSUE_TEMPLATE/style_issue.md
.vim/bundle/black/.github/PULL_REQUEST_TEMPLATE.md
.vim/bundle/black/.github/dependabot.yml [new file with mode: 0644]
.vim/bundle/black/.github/workflows/changelog.yml
.vim/bundle/black/.github/workflows/diff_shades.yml [new file with mode: 0644]
.vim/bundle/black/.github/workflows/diff_shades_comment.yml [new file with mode: 0644]
.vim/bundle/black/.github/workflows/doc.yml
.vim/bundle/black/.github/workflows/docker.yml
.vim/bundle/black/.github/workflows/fuzz.yml
.vim/bundle/black/.github/workflows/lint.yml
.vim/bundle/black/.github/workflows/primer.yml [deleted file]
.vim/bundle/black/.github/workflows/pypi_upload.yml
.vim/bundle/black/.github/workflows/test.yml
.vim/bundle/black/.github/workflows/upload_binary.yml
.vim/bundle/black/.github/workflows/uvloop_test.yml [deleted file]
.vim/bundle/black/.gitignore
.vim/bundle/black/.pre-commit-config.yaml
.vim/bundle/black/.pre-commit-hooks.yaml
.vim/bundle/black/.readthedocs.yaml
.vim/bundle/black/AUTHORS.md
.vim/bundle/black/CHANGES.md
.vim/bundle/black/CITATION.cff [new file with mode: 0644]
.vim/bundle/black/Dockerfile
.vim/bundle/black/MANIFEST.in [deleted file]
.vim/bundle/black/Pipfile [deleted file]
.vim/bundle/black/Pipfile.lock [deleted file]
.vim/bundle/black/README.md
.vim/bundle/black/SECURITY.md [new file with mode: 0644]
.vim/bundle/black/action.yml
.vim/bundle/black/action/main.py
.vim/bundle/black/autoload/black.vim
.vim/bundle/black/docs/_static/custom.css [deleted file]
.vim/bundle/black/docs/compatible_configs/pylint/pylintrc
.vim/bundle/black/docs/compatible_configs/pylint/pyproject.toml
.vim/bundle/black/docs/compatible_configs/pylint/setup.cfg
.vim/bundle/black/docs/conf.py
.vim/bundle/black/docs/contributing/gauging_changes.md
.vim/bundle/black/docs/contributing/index.md [new file with mode: 0644]
.vim/bundle/black/docs/contributing/index.rst [deleted file]
.vim/bundle/black/docs/contributing/issue_triage.md
.vim/bundle/black/docs/contributing/reference/reference_classes.rst
.vim/bundle/black/docs/contributing/reference/reference_exceptions.rst
.vim/bundle/black/docs/contributing/reference/reference_functions.rst
.vim/bundle/black/docs/contributing/reference/reference_summary.rst
.vim/bundle/black/docs/contributing/release_process.md
.vim/bundle/black/docs/contributing/the_basics.md
.vim/bundle/black/docs/faq.md
.vim/bundle/black/docs/getting_started.md
.vim/bundle/black/docs/guides/index.md [new file with mode: 0644]
.vim/bundle/black/docs/guides/index.rst [deleted file]
.vim/bundle/black/docs/guides/introducing_black_to_your_project.md
.vim/bundle/black/docs/guides/using_black_with_other_tools.md
.vim/bundle/black/docs/index.md [new file with mode: 0644]
.vim/bundle/black/docs/index.rst [deleted file]
.vim/bundle/black/docs/integrations/editors.md
.vim/bundle/black/docs/integrations/github_actions.md
.vim/bundle/black/docs/integrations/index.md [new file with mode: 0644]
.vim/bundle/black/docs/integrations/index.rst [deleted file]
.vim/bundle/black/docs/integrations/source_version_control.md
.vim/bundle/black/docs/license.md [new file with mode: 0644]
.vim/bundle/black/docs/license.rst [deleted file]
.vim/bundle/black/docs/requirements.txt
.vim/bundle/black/docs/the_black_code_style/current_style.md
.vim/bundle/black/docs/the_black_code_style/future_style.md
.vim/bundle/black/docs/the_black_code_style/index.md [new file with mode: 0644]
.vim/bundle/black/docs/the_black_code_style/index.rst [deleted file]
.vim/bundle/black/docs/usage_and_configuration/black_as_a_server.md
.vim/bundle/black/docs/usage_and_configuration/black_docker_image.md [new file with mode: 0644]
.vim/bundle/black/docs/usage_and_configuration/file_collection_and_discovery.md
.vim/bundle/black/docs/usage_and_configuration/index.md [new file with mode: 0644]
.vim/bundle/black/docs/usage_and_configuration/index.rst [deleted file]
.vim/bundle/black/docs/usage_and_configuration/the_basics.md
.vim/bundle/black/gallery/gallery.py
.vim/bundle/black/mypy.ini [deleted file]
.vim/bundle/black/plugin/black.vim
.vim/bundle/black/pyproject.toml
.vim/bundle/black/scripts/check_pre_commit_rev_in_example.py
.vim/bundle/black/scripts/check_version_in_basics_example.py
.vim/bundle/black/scripts/diff_shades_gha_helper.py [new file with mode: 0644]
.vim/bundle/black/scripts/fuzz.py [moved from .vim/bundle/black/fuzz.py with 86% similarity]
.vim/bundle/black/scripts/make_width_table.py [new file with mode: 0644]
.vim/bundle/black/scripts/migrate-black.py [new file with mode: 0755]
.vim/bundle/black/setup.cfg [deleted file]
.vim/bundle/black/setup.py [deleted file]
.vim/bundle/black/src/black/__init__.py
.vim/bundle/black/src/black/_width_table.py [new file with mode: 0644]
.vim/bundle/black/src/black/brackets.py
.vim/bundle/black/src/black/cache.py
.vim/bundle/black/src/black/comments.py
.vim/bundle/black/src/black/concurrency.py
.vim/bundle/black/src/black/const.py
.vim/bundle/black/src/black/debug.py
.vim/bundle/black/src/black/files.py
.vim/bundle/black/src/black/handle_ipynb_magics.py
.vim/bundle/black/src/black/linegen.py
.vim/bundle/black/src/black/lines.py
.vim/bundle/black/src/black/mode.py
.vim/bundle/black/src/black/nodes.py
.vim/bundle/black/src/black/numerics.py
.vim/bundle/black/src/black/output.py
.vim/bundle/black/src/black/parsing.py
.vim/bundle/black/src/black/report.py
.vim/bundle/black/src/black/rusty.py
.vim/bundle/black/src/black/strings.py
.vim/bundle/black/src/black/trans.py
.vim/bundle/black/src/black_primer/cli.py [deleted file]
.vim/bundle/black/src/black_primer/lib.py [deleted file]
.vim/bundle/black/src/black_primer/primer.json [deleted file]
.vim/bundle/black/src/blackd/__init__.py
.vim/bundle/black/src/blackd/__main__.py [new file with mode: 0644]
.vim/bundle/black/src/blackd/middlewares.py
.vim/bundle/black/src/blib2to3/Grammar.txt
.vim/bundle/black/src/blib2to3/README
.vim/bundle/black/src/blib2to3/pgen2/conv.py
.vim/bundle/black/src/blib2to3/pgen2/driver.py
.vim/bundle/black/src/blib2to3/pgen2/grammar.py
.vim/bundle/black/src/blib2to3/pgen2/literals.py
.vim/bundle/black/src/blib2to3/pgen2/parse.py
.vim/bundle/black/src/blib2to3/pgen2/pgen.py
.vim/bundle/black/src/blib2to3/pgen2/token.py
.vim/bundle/black/src/blib2to3/pgen2/tokenize.py
.vim/bundle/black/src/blib2to3/pygram.py
.vim/bundle/black/src/blib2to3/pytree.py
.vim/bundle/black/test_requirements.txt
.vim/bundle/black/tests/conftest.py
.vim/bundle/black/tests/data/cases/attribute_access_on_number_literals.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/beginning_backslash.py [moved from .vim/bundle/black/tests/data/beginning_backslash.py with 100% similarity]
.vim/bundle/black/tests/data/cases/bracketmatch.py [moved from .vim/bundle/black/tests/data/bracketmatch.py with 100% similarity]
.vim/bundle/black/tests/data/cases/class_blank_parentheses.py [moved from .vim/bundle/black/tests/data/class_blank_parentheses.py with 100% similarity]
.vim/bundle/black/tests/data/cases/class_methods_new_line.py [moved from .vim/bundle/black/tests/data/class_methods_new_line.py with 100% similarity]
.vim/bundle/black/tests/data/cases/collections.py [moved from .vim/bundle/black/tests/data/collections.py with 100% similarity]
.vim/bundle/black/tests/data/cases/comment_after_escaped_newline.py [moved from .vim/bundle/black/tests/data/comment_after_escaped_newline.py with 100% similarity]
.vim/bundle/black/tests/data/cases/comments.py [moved from .vim/bundle/black/tests/data/comments.py with 100% similarity]
.vim/bundle/black/tests/data/cases/comments2.py [moved from .vim/bundle/black/tests/data/comments2.py with 98% similarity]
.vim/bundle/black/tests/data/cases/comments3.py [moved from .vim/bundle/black/tests/data/comments3.py with 99% similarity]
.vim/bundle/black/tests/data/cases/comments4.py [moved from .vim/bundle/black/tests/data/comments4.py with 98% similarity]
.vim/bundle/black/tests/data/cases/comments5.py [moved from .vim/bundle/black/tests/data/comments5.py with 86% similarity]
.vim/bundle/black/tests/data/cases/comments6.py [moved from .vim/bundle/black/tests/data/comments6.py with 100% similarity]
.vim/bundle/black/tests/data/cases/comments8.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/comments9.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/comments_non_breaking_space.py [moved from .vim/bundle/black/tests/data/comments_non_breaking_space.py with 100% similarity]
.vim/bundle/black/tests/data/cases/composition.py [moved from .vim/bundle/black/tests/data/composition.py with 100% similarity]
.vim/bundle/black/tests/data/cases/composition_no_trailing_comma.py [moved from .vim/bundle/black/tests/data/composition_no_trailing_comma.py with 100% similarity]
.vim/bundle/black/tests/data/cases/conditional_expression.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/docstring.py [moved from .vim/bundle/black/tests/data/docstring.py with 72% similarity]
.vim/bundle/black/tests/data/cases/docstring_no_extra_empty_line_before_eof.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/docstring_no_string_normalization.py [moved from .vim/bundle/black/tests/data/docstring_no_string_normalization.py with 98% similarity]
.vim/bundle/black/tests/data/cases/docstring_preview.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/docstring_preview_no_string_normalization.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/empty_lines.py [moved from .vim/bundle/black/tests/data/empty_lines.py with 99% similarity]
.vim/bundle/black/tests/data/cases/expression.diff [moved from .vim/bundle/black/tests/data/expression.diff with 91% similarity]
.vim/bundle/black/tests/data/cases/expression.py [moved from .vim/bundle/black/tests/data/expression.py with 95% similarity]
.vim/bundle/black/tests/data/cases/fmtonoff.py [moved from .vim/bundle/black/tests/data/fmtonoff.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtonoff2.py [moved from .vim/bundle/black/tests/data/fmtonoff2.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtonoff3.py [moved from .vim/bundle/black/tests/data/fmtonoff3.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtonoff4.py [moved from .vim/bundle/black/tests/data/fmtonoff4.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtonoff5.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/fmtpass_imports.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/fmtskip.py [moved from .vim/bundle/black/tests/data/fmtskip.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtskip2.py [moved from .vim/bundle/black/tests/data/fmtskip2.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtskip3.py [moved from .vim/bundle/black/tests/data/fmtskip3.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtskip4.py [moved from .vim/bundle/black/tests/data/fmtskip4.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtskip5.py [moved from .vim/bundle/black/tests/data/fmtskip5.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtskip6.py [moved from .vim/bundle/black/tests/data/fmtskip6.py with 100% similarity]
.vim/bundle/black/tests/data/cases/fmtskip7.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/fmtskip8.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/fstring.py [moved from .vim/bundle/black/tests/data/fstring.py with 82% similarity]
.vim/bundle/black/tests/data/cases/funcdef_return_type_trailing_comma.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/function.py [moved from .vim/bundle/black/tests/data/function.py with 100% similarity]
.vim/bundle/black/tests/data/cases/function2.py [moved from .vim/bundle/black/tests/data/function2.py with 52% similarity]
.vim/bundle/black/tests/data/cases/function_trailing_comma.py [moved from .vim/bundle/black/tests/data/function_trailing_comma.py with 68% similarity]
.vim/bundle/black/tests/data/cases/ignore_pyi.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/import_spacing.py [moved from .vim/bundle/black/tests/data/import_spacing.py with 100% similarity]
.vim/bundle/black/tests/data/cases/linelength6.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/long_strings_flag_disabled.py [moved from .vim/bundle/black/tests/data/long_strings_flag_disabled.py with 97% similarity]
.vim/bundle/black/tests/data/cases/module_docstring_1.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/module_docstring_2.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/module_docstring_3.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/module_docstring_4.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/multiline_consecutive_open_parentheses_ignore.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/nested_stub.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/numeric_literals.py [moved from .vim/bundle/black/tests/data/numeric_literals.py with 91% similarity]
.vim/bundle/black/tests/data/cases/numeric_literals_skip_underscores.py [moved from .vim/bundle/black/tests/data/numeric_literals_skip_underscores.py with 77% similarity]
.vim/bundle/black/tests/data/cases/one_element_subscript.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/parenthesized_context_managers.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pattern_matching_complex.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pattern_matching_extras.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pattern_matching_generic.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pattern_matching_simple.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pattern_matching_style.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pep604_union_types_line_breaks.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pep_570.py [moved from .vim/bundle/black/tests/data/pep_570.py with 95% similarity]
.vim/bundle/black/tests/data/cases/pep_572.py [moved from .vim/bundle/black/tests/data/pep_572.py with 94% similarity]
.vim/bundle/black/tests/data/cases/pep_572_do_not_remove_parens.py [moved from .vim/bundle/black/tests/data/pep_572_do_not_remove_parens.py with 89% similarity]
.vim/bundle/black/tests/data/cases/pep_572_py310.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pep_572_py39.py [moved from .vim/bundle/black/tests/data/pep_572_py39.py with 78% similarity]
.vim/bundle/black/tests/data/cases/pep_572_remove_parens.py [moved from .vim/bundle/black/tests/data/pep_572_remove_parens.py with 65% similarity]
.vim/bundle/black/tests/data/cases/pep_604.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pep_646.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pep_654.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/pep_654_style.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/power_op_newline.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/power_op_spacing.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/prefer_rhs_split_reformatted.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_async_stmts.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_cantfit.py [moved from .vim/bundle/black/tests/data/cantfit.py with 99% similarity]
.vim/bundle/black/tests/data/cases/preview_comments7.py [moved from .vim/bundle/black/tests/data/comments7.py with 74% similarity]
.vim/bundle/black/tests/data/cases/preview_context_managers_38.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_context_managers_39.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_310.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_311.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_38.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_39.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_dummy_implementations.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_format_unicode_escape_seq.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_long_dict_values.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_long_strings.py [moved from .vim/bundle/black/tests/data/long_strings.py with 76% similarity]
.vim/bundle/black/tests/data/cases/preview_long_strings__east_asian_width.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_long_strings__edge_case.py [moved from .vim/bundle/black/tests/data/long_strings__edge_case.py with 99% similarity]
.vim/bundle/black/tests/data/cases/preview_long_strings__regression.py [moved from .vim/bundle/black/tests/data/long_strings__regression.py with 89% similarity]
.vim/bundle/black/tests/data/cases/preview_long_strings__type_annotations.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_multiline_strings.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_no_blank_line_before_docstring.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_pep_572.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_percent_precedence.py [moved from .vim/bundle/black/tests/data/percent_precedence.py with 96% similarity]
.vim/bundle/black/tests/data/cases/preview_power_op_spacing.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_prefer_rhs_split.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_return_annotation_brackets_string.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/preview_trailing_comma.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/py310_pep572.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/python37.py [moved from .vim/bundle/black/tests/data/python37.py with 95% similarity]
.vim/bundle/black/tests/data/cases/python38.py [moved from .vim/bundle/black/tests/data/python38.py with 93% similarity]
.vim/bundle/black/tests/data/cases/python39.py [moved from .vim/bundle/black/tests/data/python39.py with 91% similarity]
.vim/bundle/black/tests/data/cases/remove_await_parens.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/remove_except_parens.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/remove_for_brackets.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/remove_newline_after_code_block_open.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/remove_newline_after_match.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/remove_parens.py [moved from .vim/bundle/black/tests/data/remove_parens.py with 100% similarity]
.vim/bundle/black/tests/data/cases/remove_with_brackets.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/return_annotation_brackets.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/skip_magic_trailing_comma.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/slices.py [moved from .vim/bundle/black/tests/data/slices.py with 94% similarity]
.vim/bundle/black/tests/data/cases/starred_for_target.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/string_prefixes.py [moved from .vim/bundle/black/tests/data/string_prefixes.py with 51% similarity]
.vim/bundle/black/tests/data/cases/stub.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/torture.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens1.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens2.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens3.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/trailing_commas_in_leading_parts.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/tricky_unicode_symbols.py [moved from .vim/bundle/black/tests/data/tricky_unicode_symbols.py with 76% similarity]
.vim/bundle/black/tests/data/cases/tupleassign.py [moved from .vim/bundle/black/tests/data/tupleassign.py with 100% similarity]
.vim/bundle/black/tests/data/cases/type_aliases.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/type_comment_syntax_error.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/type_params.py [new file with mode: 0644]
.vim/bundle/black/tests/data/cases/whitespace.py [new file with mode: 0644]
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/.gitignore [new file with mode: 0644]
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir1/a.py [moved from .vim/bundle/black/src/black_primer/__init__.py with 100% similarity]
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir1/b.py [new file with mode: 0644]
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/a.py [new file with mode: 0644]
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/b.py [new file with mode: 0644]
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/a.py [new file with mode: 0644]
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore [new file with mode: 0644]
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py [new file with mode: 0644]
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py [new file with mode: 0644]
.vim/bundle/black/tests/data/jupyter/non_python_notebook.ipynb [moved from .vim/bundle/black/tests/data/non_python_notebook.ipynb with 100% similarity]
.vim/bundle/black/tests/data/jupyter/notebook_empty_metadata.ipynb [moved from .vim/bundle/black/tests/data/notebook_empty_metadata.ipynb with 100% similarity]
.vim/bundle/black/tests/data/jupyter/notebook_no_trailing_newline.ipynb [moved from .vim/bundle/black/tests/data/notebook_no_trailing_newline.ipynb with 100% similarity]
.vim/bundle/black/tests/data/jupyter/notebook_trailing_newline.ipynb [moved from .vim/bundle/black/tests/data/notebook_trailing_newline.ipynb with 100% similarity]
.vim/bundle/black/tests/data/jupyter/notebook_which_cant_be_parsed.ipynb [moved from .vim/bundle/black/tests/data/notebook_which_cant_be_parsed.ipynb with 100% similarity]
.vim/bundle/black/tests/data/jupyter/notebook_without_changes.ipynb [moved from .vim/bundle/black/tests/data/notebook_without_changes.ipynb with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/async_as_identifier.py [moved from .vim/bundle/black/tests/data/async_as_identifier.py with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/blackd_diff.diff [moved from .vim/bundle/black/tests/data/blackd_diff.diff with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/blackd_diff.py [moved from .vim/bundle/black/tests/data/blackd_diff.py with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/debug_visitor.out [moved from .vim/bundle/black/tests/data/debug_visitor.out with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/debug_visitor.py [moved from .vim/bundle/black/tests/data/debug_visitor.py with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/decorators.py [moved from .vim/bundle/black/tests/data/decorators.py with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff [moved from .vim/bundle/black/tests/data/expression_skip_magic_trailing_comma.diff with 91% similarity]
.vim/bundle/black/tests/data/miscellaneous/force_py36.py [moved from .vim/bundle/black/tests/data/force_py36.py with 88% similarity]
.vim/bundle/black/tests/data/miscellaneous/force_pyi.py [moved from .vim/bundle/black/tests/data/force_pyi.py with 98% similarity]
.vim/bundle/black/tests/data/miscellaneous/invalid_header.py [new file with mode: 0644]
.vim/bundle/black/tests/data/miscellaneous/missing_final_newline.diff [moved from .vim/bundle/black/tests/data/missing_final_newline.diff with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/missing_final_newline.py [moved from .vim/bundle/black/tests/data/missing_final_newline.py with 100% similarity]
.vim/bundle/black/tests/data/miscellaneous/pattern_matching_invalid.py [new file with mode: 0644]
.vim/bundle/black/tests/data/miscellaneous/python2_detection.py [new file with mode: 0644]
.vim/bundle/black/tests/data/miscellaneous/string_quotes.py [moved from .vim/bundle/black/tests/data/string_quotes.py with 99% similarity]
.vim/bundle/black/tests/data/numeric_literals_py2.py [deleted file]
.vim/bundle/black/tests/data/pep_572_py310.py [deleted file]
.vim/bundle/black/tests/data/project_metadata/both_pyproject.toml [new file with mode: 0644]
.vim/bundle/black/tests/data/project_metadata/neither_pyproject.toml [new file with mode: 0644]
.vim/bundle/black/tests/data/project_metadata/only_black_pyproject.toml [new file with mode: 0644]
.vim/bundle/black/tests/data/project_metadata/only_metadata_pyproject.toml [new file with mode: 0644]
.vim/bundle/black/tests/data/python2.py [deleted file]
.vim/bundle/black/tests/data/python2_print_function.py [deleted file]
.vim/bundle/black/tests/data/python2_unicode_literals.py [deleted file]
.vim/bundle/black/tests/data/raw_docstring.py [new file with mode: 0644]
.vim/bundle/black/tests/data/stub.pyi [deleted file]
.vim/bundle/black/tests/data/trailing_comma_optional_parens1.py [deleted file]
.vim/bundle/black/tests/data/trailing_comma_optional_parens2.py [deleted file]
.vim/bundle/black/tests/data/trailing_comma_optional_parens3.py [deleted file]
.vim/bundle/black/tests/optional.py
.vim/bundle/black/tests/test.toml
.vim/bundle/black/tests/test_black.py
.vim/bundle/black/tests/test_blackd.py
.vim/bundle/black/tests/test_format.py
.vim/bundle/black/tests/test_ipynb.py
.vim/bundle/black/tests/test_no_ipynb.py
.vim/bundle/black/tests/test_primer.py [deleted file]
.vim/bundle/black/tests/test_trans.py [new file with mode: 0644]
.vim/bundle/black/tests/util.py
.vim/bundle/black/tox.ini

diff --git a/.vim/bundle/black/.coveragerc b/.vim/bundle/black/.coveragerc
deleted file mode 100644 (file)
index 5577e49..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-[report]
-omit =
-  src/blib2to3/*
-  tests/data/*
-  */site-packages/*
-  .tox/*
-
-[run]
-relative_files = True
index ae11a13347c999a4b1f34f1d77f95f157172de9c..85f51cf9f05a4e6dd004afd7d64e9a54457f1901 100644 (file)
@@ -1,5 +1,6 @@
 [flake8]
-ignore = E203, E266, E501, W503
+# B905 should be enabled when we drop support for 3.9
+ignore = E203, E266, E501, E704, W503, B905, B907
 # line length is intentionally set to 80 here because black uses Bugbear
 # See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details
 max-line-length = 80
diff --git a/.vim/bundle/black/.git_archival.txt b/.vim/bundle/black/.git_archival.txt
new file mode 100644 (file)
index 0000000..8fb235d
--- /dev/null
@@ -0,0 +1,4 @@
+node: $Format:%H$
+node-date: $Format:%cI$
+describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
+ref-names: $Format:%D$
diff --git a/.vim/bundle/black/.gitattributes b/.vim/bundle/black/.gitattributes
new file mode 100644 (file)
index 0000000..00a7b00
--- /dev/null
@@ -0,0 +1 @@
+.git_archival.txt  export-subst
index e8d232c8b34c840846090534b551a073f5fb9923..48aa9291b0523359cd34eb3924bd7a724aae4e58 100644 (file)
@@ -6,41 +6,58 @@ labels: "T: bug"
 assignees: ""
 ---
 
+<!--
+Please make sure that the bug is not already fixed either in newer versions or the
+current development version. To confirm this, you have three options:
+
+1. Update Black's version if a newer release exists: `pip install -U black`
+2. Use the online formatter at <https://black.vercel.app/?version=main>, which will use
+   the latest main branch.
+3. Or run _Black_ on your machine:
+   - create a new virtualenv (make sure it's the same Python version);
+   - clone this repository;
+   - run `pip install -e .[d]`;
+   - run `pip install -r test_requirements.txt`
+   - make sure it's sane by running `python -m pytest`; and
+   - run `black` like you did last time.
+-->
+
 **Describe the bug**
 
 <!-- A clear and concise description of what the bug is. -->
 
 **To Reproduce**
 
-<!-- Steps to reproduce the behavior:
+<!--
+Minimal steps to reproduce the behavior with source code and Black's configuration.
+-->
 
-For example:
-1. Take this file '...'
-1. Run _Black_ on it with these arguments '...'
-1. See error -->
+For example, take this code:
 
-**Expected behavior**
+```python
+this = "code"
+```
 
-<!-- A clear and concise description of what you expected to happen. -->
+And run it with these arguments:
 
-**Environment (please complete the following information):**
+```sh
+$ black file.py --target-version py39
+```
 
-- Version: <!-- e.g. [main] -->
-- OS and Python version: <!-- e.g. [Linux/Python 3.7.4rc1] -->
+The resulting error is:
 
-**Does this bug also happen on main?**
+> cannot format file.py: INTERNAL ERROR: ...
 
-<!-- To answer this, you have two options:
+**Expected behavior**
 
-1. Use the online formatter at <https://black.vercel.app/?version=main>, which will use
-   the latest main branch.
-1. Or run _Black_ on your machine:
-   - create a new virtualenv (make sure it's the same Python version);
-   - clone this repository;
-   - run `pip install -e .[d,python2]`;
-   - run `pip install -r test_requirements.txt`
-   - make sure it's sane by running `python -m pytest`; and
-   - run `black` like you did last time. -->
+<!-- A clear and concise description of what you expected to happen. -->
+
+**Environment**
+
+<!-- Please complete the following information: -->
+
+- Black's version: <!-- e.g. [main] -->
+- OS and Python version: <!-- e.g. [Linux/Python 3.7.4rc1] -->
 
 **Additional context**
 
index 2e4343a3527be07f0c1f5500f63c9c7ed1b96a33..a9ce85fd977a1144f84da86540e7ae9708f9c22f 100644 (file)
@@ -1,8 +1,8 @@
 ---
-name: Style issue
-about: Help us improve the Black style
+name: Code style issue
+about: Help us improve the Black code style
 title: ""
-labels: "T: design"
+labels: "T: style"
 assignees: ""
 ---
 
index 833cd1641347f1a64d890a318362e2c21ae68478..a039718cd7053d44610ce12c850ad520d999dc97 100644 (file)
@@ -18,7 +18,7 @@
     Tests are required for bugfixes and new features. Documentation changes
     are necessary for formatting and most enhancement changes. -->
 
-- [ ] Add a CHANGELOG entry if necessary?
+- [ ] Add an entry in `CHANGES.md` if necessary?
 - [ ] Add / update tests if necessary?
 - [ ] Add new / update outdated documentation?
 
diff --git a/.vim/bundle/black/.github/dependabot.yml b/.vim/bundle/black/.github/dependabot.yml
new file mode 100644 (file)
index 0000000..325cb31
--- /dev/null
@@ -0,0 +1,17 @@
+# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    # Workflow files in .github/workflows will be checked
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    labels: ["skip news", "C: dependencies"]
+
+  - package-ecosystem: "pip"
+    directory: "docs/"
+    schedule:
+      interval: "weekly"
+    labels: ["skip news", "C: dependencies", "T: documentation"]
+    reviewers: ["ichard26"]
index d7ee50558d3655e5f02b6668f162cb90777d8073..a1804597d7d22fb60bde1f425a94f88c2c85e36a 100644 (file)
@@ -4,6 +4,9 @@ on:
   pull_request:
     types: [opened, synchronize, labeled, unlabeled, reopened]
 
+permissions:
+  contents: read
+
 jobs:
   build:
     name: Changelog Entry Check
@@ -11,11 +14,11 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
       - name: Grep CHANGES.md for PR number
         if: contains(github.event.pull_request.labels.*.name, 'skip news') != true
         run: |
           grep -Pz "\((\n\s*)?#${{ github.event.pull_request.number }}(\n\s*)?\)" CHANGES.md || \
-          (echo "Please add '(#${{ github.event.pull_request.number }})' change line to CHANGES.md" && \
+          (echo "Please add '(#${{ github.event.pull_request.number }})' change line to CHANGES.md (or if appropriate, ask a maintainer to add the 'skip news' label)" && \
           exit 1)
diff --git a/.vim/bundle/black/.github/workflows/diff_shades.yml b/.vim/bundle/black/.github/workflows/diff_shades.yml
new file mode 100644 (file)
index 0000000..97db907
--- /dev/null
@@ -0,0 +1,155 @@
+name: diff-shades
+
+on:
+  push:
+    branches: [main]
+    paths: ["src/**", "pyproject.toml", ".github/workflows/*"]
+
+  pull_request:
+    paths: ["src/**", "pyproject.toml", ".github/workflows/*"]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  configure:
+    runs-on: ubuntu-latest
+    outputs:
+      matrix: ${{ steps.set-config.outputs.matrix }}
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "*"
+
+      - name: Install diff-shades and support dependencies
+        run: |
+          python -m pip install 'click==8.1.3' packaging urllib3
+          python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip
+
+      - name: Calculate run configuration & metadata
+        id: set-config
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >
+          python scripts/diff_shades_gha_helper.py config ${{ github.event_name }} ${{ matrix.mode }}
+
+  analysis:
+    name: analysis / ${{ matrix.mode }}
+    needs: configure
+    runs-on: ubuntu-latest
+    env:
+      HATCH_BUILD_HOOKS_ENABLE: "1"
+      # Clang is less picky with the C code it's given than gcc (and may
+      # generate faster binaries too).
+      CC: clang-14
+    strategy:
+      fail-fast: false
+      matrix:
+        include: ${{ fromJson(needs.configure.outputs.matrix )}}
+
+    steps:
+      - name: Checkout this repository (full clone)
+        uses: actions/checkout@v4
+        with:
+          # The baseline revision could be rather old so a full clone is ideal.
+          fetch-depth: 0
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "*"
+
+      - name: Install diff-shades and support dependencies
+        run: |
+          python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip
+          python -m pip install 'click==8.1.3' packaging urllib3
+          # After checking out old revisions, this might not exist so we'll use a copy.
+          cat scripts/diff_shades_gha_helper.py > helper.py
+          git config user.name "diff-shades-gha"
+          git config user.email "diff-shades-gha@example.com"
+
+      - name: Attempt to use cached baseline analysis
+        id: baseline-cache
+        uses: actions/cache@v3
+        with:
+          path: ${{ matrix.baseline-analysis }}
+          key: ${{ matrix.baseline-cache-key }}
+
+      - name: Build and install baseline revision
+        if: steps.baseline-cache.outputs.cache-hit != 'true'
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >
+          ${{ matrix.baseline-setup-cmd }}
+          && python -m pip install .
+
+      - name: Analyze baseline revision
+        if: steps.baseline-cache.outputs.cache-hit != 'true'
+        run: >
+          diff-shades analyze -v --work-dir projects-cache/
+          ${{ matrix.baseline-analysis }} ${{ matrix.force-flag }}
+
+      - name: Build and install target revision
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >
+          ${{ matrix.target-setup-cmd }}
+          && python -m pip install .
+
+      - name: Analyze target revision
+        run: >
+          diff-shades analyze -v --work-dir projects-cache/
+          ${{ matrix.target-analysis }} --repeat-projects-from
+          ${{ matrix.baseline-analysis }} ${{ matrix.force-flag }}
+
+      - name: Generate HTML diff report
+        run: >
+          diff-shades --dump-html diff.html compare --diff
+          ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }}
+
+      - name: Upload diff report
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ matrix.mode }}-diff.html
+          path: diff.html
+
+      - name: Upload baseline analysis
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ matrix.baseline-analysis }}
+          path: ${{ matrix.baseline-analysis }}
+
+      - name: Upload target analysis
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ matrix.target-analysis }}
+          path: ${{ matrix.target-analysis }}
+
+      - name: Generate summary file (PR only)
+        if: github.event_name == 'pull_request' && matrix.mode == 'preview-changes'
+        run: >
+          python helper.py comment-body
+          ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }}
+          ${{ matrix.baseline-sha }} ${{ matrix.target-sha }}
+          ${{ github.event.pull_request.number }}
+
+      - name: Upload summary file (PR only)
+        if: github.event_name == 'pull_request' && matrix.mode == 'preview-changes'
+        uses: actions/upload-artifact@v3
+        with:
+          name: .pr-comment.json
+          path: .pr-comment.json
+
+      - name: Verify zero changes (PR only)
+        if: matrix.mode == 'assert-no-changes'
+        run: >
+          diff-shades compare --check ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }}
+          || (echo "Please verify you didn't change the stable code style unintentionally!" && exit 1)
+
+      - name: Check for failed files for target revision
+        # Even if the previous step failed, we should still check for failed files.
+        if: always()
+        run: >
+          diff-shades show-failed --check --show-log ${{ matrix.target-analysis }}
diff --git a/.vim/bundle/black/.github/workflows/diff_shades_comment.yml b/.vim/bundle/black/.github/workflows/diff_shades_comment.yml
new file mode 100644 (file)
index 0000000..b86bd93
--- /dev/null
@@ -0,0 +1,49 @@
+name: diff-shades-comment
+
+on:
+  workflow_run:
+    workflows: [diff-shades]
+    types: [completed]
+
+permissions:
+  pull-requests: write
+
+jobs:
+  comment:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "*"
+
+      - name: Install support dependencies
+        run: |
+          python -m pip install pip --upgrade
+          python -m pip install click packaging urllib3
+
+      - name: Get details from initial workflow run
+        id: metadata
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >
+          python scripts/diff_shades_gha_helper.py comment-details
+          ${{github.event.workflow_run.id }}
+
+      - name: Try to find pre-existing PR comment
+        if: steps.metadata.outputs.needs-comment == 'true'
+        id: find-comment
+        uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1
+        with:
+          issue-number: ${{ steps.metadata.outputs.pr-number }}
+          comment-author: "github-actions[bot]"
+          body-includes: "diff-shades"
+
+      - name: Create or update PR comment
+        if: steps.metadata.outputs.needs-comment == 'true'
+        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa
+        with:
+          comment-id: ${{ steps.find-comment.outputs.comment-id }}
+          issue-number: ${{ steps.metadata.outputs.pr-number }}
+          body: ${{ steps.metadata.outputs.comment-body }}
+          edit-mode: replace
index 5689d2887c4fdf5ac70a44e3b15808b657dea223..fa3d87c70f5b6d61cd2bdae4ecb9b0fd54d36805 100644 (file)
@@ -1,7 +1,10 @@
-name: Documentation Build
+name: Documentation
 
 on: [push, pull_request]
 
+permissions:
+  contents: read
+
 jobs:
   build:
     # We want to run on external PRs, but not on our own internal PRs as they'll be run
@@ -18,10 +21,12 @@ jobs:
 
     runs-on: ${{ matrix.os }}
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
       - name: Set up latest Python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
+        with:
+          python-version: "*"
 
       - name: Install dependencies
         run: |
index 0825385c6c08989d763a7bb0dea24df6f8cf471f..ee858236fcf1780678d3c223421711e7ca0f7a56 100644 (file)
@@ -7,22 +7,25 @@ on:
   release:
     types: [published]
 
+permissions:
+  contents: read
+
 jobs:
   docker:
     if: github.repository == 'psf/black'
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v1
+        uses: docker/setup-qemu-action@v3
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v1
+        uses: docker/setup-buildx-action@v3
 
       - name: Login to DockerHub
-        uses: docker/login-action@v1
+        uses: docker/login-action@v3
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -33,7 +36,7 @@ jobs:
           latest_non_release)" >> $GITHUB_ENV
 
       - name: Build and push
-        uses: docker/build-push-action@v2
+        uses: docker/build-push-action@v5
         with:
           context: .
           platforms: linux/amd64,linux/arm64
@@ -41,13 +44,26 @@ jobs:
           tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }}
 
       - name: Build and push latest_release tag
-        if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
-        uses: docker/build-push-action@v2
+        if:
+          ${{ github.event_name == 'release' && github.event.action == 'published' &&
+          !github.event.release.prerelease }}
+        uses: docker/build-push-action@v5
         with:
           context: .
           platforms: linux/amd64,linux/arm64
           push: true
           tags: pyfound/black:latest_release
 
+      - name: Build and push latest_prerelease tag
+        if:
+          ${{ github.event_name == 'release' && github.event.action == 'published' &&
+          github.event.release.prerelease }}
+        uses: docker/build-push-action@v5
+        with:
+          context: .
+          platforms: linux/amd64,linux/arm64
+          push: true
+          tags: pyfound/black:latest_prerelease
+
       - name: Image digest
         run: echo ${{ steps.docker_build.outputs.digest }}
index 146277a7312f0fc5a5a98eee561e65b6a9d79dee..1b5a50c0e0b4a32faed252cedfb6fea19c9020b7 100644 (file)
@@ -2,6 +2,13 @@ name: Fuzz
 
 on: [push, pull_request]
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+
 jobs:
   build:
     # We want to run on external PRs, but not on our own internal PRs as they'll be run
@@ -15,13 +22,13 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python-version }}
 
index 51f6d02e2e6bccb67e81886c9d81f28c5450c383..3eaf5785f5a675453bcd7c82a5c06b722ea6fa4b 100644 (file)
@@ -14,15 +14,29 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
-      - name: Set up Python
-        uses: actions/setup-python@v2
+      - name: Assert PR target is main
+        if: github.event_name == 'pull_request' && github.repository == 'psf/black'
+        run: |
+          if [ "$GITHUB_BASE_REF" != "main" ]; then
+              echo "::error::PR targeting '$GITHUB_BASE_REF', please refile targeting 'main'." && exit 1
+          fi
+
+      - name: Set up latest Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "*"
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
           python -m pip install -e '.[d]'
+          python -m pip install tox
 
-      - name: Lint
-        uses: pre-commit/action@v2.0.2
+      - name: Run pre-commit hooks
+        uses: pre-commit/action@v3.0.0
+
+      - name: Format ourselves
+        run: |
+          tox -e run_self
diff --git a/.vim/bundle/black/.github/workflows/primer.yml b/.vim/bundle/black/.github/workflows/primer.yml
deleted file mode 100644 (file)
index 5fa6ac0..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Primer
-
-on:
-  push:
-    paths-ignore:
-      - "docs/**"
-      - "*.md"
-
-  pull_request:
-    paths-ignore:
-      - "docs/**"
-      - "*.md"
-
-jobs:
-  build:
-    # We want to run on external PRs, but not on our own internal PRs as they'll be run
-    # by the push to the branch. Without this if check, checks are duplicated since
-    # internal PRs match both the push and pull_request events.
-    if:
-      github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
-      github.repository
-
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
-        os: [ubuntu-latest, windows-latest]
-
-    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 -e ".[d,jupyter]"
-
-      - name: Primer run
-        env:
-          pythonioencoding: utf-8
-        run: |
-          black-primer
index 201d94fd85e931ce5e71cc9f38d1b5402e5f269b..a57013d67c1b1f4c108f034583a8d44a7b8a32f4 100644 (file)
-name: pypi_upload
+name: Build and publish
 
 on:
   release:
     types: [published]
+  pull_request:
+  push:
+    branches:
+      - main
+
+permissions:
+  contents: read
 
 jobs:
-  build:
-    name: PyPI Upload
+  main:
+    name: sdist + pure wheel
     runs-on: ubuntu-latest
+    if: github.event_name == 'release'
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
-      - name: Set up Python
-        uses: actions/setup-python@v2
+      - name: Set up latest Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "*"
 
-      - name: Install latest pip, setuptools, twine + wheel
+      - name: Install latest pip, build, twine
         run: |
-          python -m pip install --upgrade pip setuptools twine wheel
+          python -m pip install --upgrade --disable-pip-version-check pip
+          python -m pip install --upgrade build twine
+
+      - name: Build wheel and source distributions
+        run: python -m build
+
+      - if: github.event_name == 'release'
+        name: Upload to PyPI via Twine
+        env:
+          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+        run: twine upload --verbose -u '__token__' dist/*
 
-      - name: Build wheels
+  generate_wheels_matrix:
+    name: generate wheels matrix
+    runs-on: ubuntu-latest
+    outputs:
+      include: ${{ steps.set-matrix.outputs.include }}
+    steps:
+      - uses: actions/checkout@v4
+      - name: Install cibuildwheel and pypyp
+        run: |
+          pipx install cibuildwheel==2.15.0
+          pipx install pypyp==1
+      - name: generate matrix
+        if: github.event_name != 'pull_request'
+        run: |
+          {
+            cibuildwheel --print-build-identifiers --platform linux \
+            | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' \
+            && cibuildwheel --print-build-identifiers --platform macos \
+            | pyp 'json.dumps({"only": x, "os": "macos-latest"})' \
+            && cibuildwheel --print-build-identifiers --platform windows \
+            | pyp 'json.dumps({"only": x, "os": "windows-latest"})'
+          } | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix
+        env:
+          CIBW_ARCHS_LINUX: x86_64
+          CIBW_ARCHS_MACOS: x86_64 arm64
+          CIBW_ARCHS_WINDOWS: AMD64
+      - name: generate matrix (PR)
+        if: github.event_name == 'pull_request'
         run: |
-          python setup.py bdist_wheel
-          python setup.py sdist
+          cibuildwheel --print-build-identifiers --platform linux \
+          | pyp 'json.dumps({"only": x, "os": "ubuntu-latest"})' \
+          | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix
+        env:
+          CIBW_BUILD: "cp38-* cp311-*"
+          CIBW_ARCHS_LINUX: x86_64
+      - id: set-matrix
+        run: echo "include=$(cat /tmp/matrix)" | tee -a $GITHUB_OUTPUT
+
+  mypyc:
+    name: mypyc wheels ${{ matrix.only }}
+    needs: generate_wheels_matrix
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include: ${{ fromJson(needs.generate_wheels_matrix.outputs.include) }}
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: pypa/cibuildwheel@v2.16.2
+        with:
+          only: ${{ matrix.only }}
+
+      - name: Upload wheels as workflow artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ matrix.name }}-mypyc-wheels
+          path: ./wheelhouse/*.whl
 
-      - name: Upload to PyPI via Twine
+      - if: github.event_name == 'release'
+        name: Upload wheels to PyPI via Twine
         env:
           TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+        run: pipx run twine upload --verbose -u '__token__' wheelhouse/*.whl
+
+  update-stable-branch:
+    name: Update stable branch
+    needs: [main, mypyc]
+    runs-on: ubuntu-latest
+    if: github.event_name == 'release'
+    permissions:
+      contents: write
+
+    steps:
+      - name: Checkout stable branch
+        uses: actions/checkout@v4
+        with:
+          ref: stable
+          fetch-depth: 0
+
+      - if: github.event_name == 'release'
+        name: Update stable branch to release tag & push
         run: |
-          twine upload --verbose -u '__token__' dist/*
+          git reset --hard ${{ github.event.release.tag_name }}
+          git push
index 296ac34a3fba34e85aac4915c20e09b1600d7702..1f33f2b814fd5f19700ea2c91325d46749a9ef2d 100644 (file)
@@ -11,8 +11,15 @@ on:
       - "docs/**"
       - "*.md"
 
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+  cancel-in-progress: true
+
 jobs:
-  build:
+  main:
     # We want to run on external PRs, but not on our own internal PRs as they'll be run
     # by the push to the branch. Without this if check, checks are duplicated since
     # internal PRs match both the push and pull_request events.
@@ -24,35 +31,38 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
         os: [ubuntu-latest, macOS-latest, windows-latest]
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python-version }}
 
-      - name: Install dependencies
+      - name: Install tox
         run: |
           python -m pip install --upgrade pip
           python -m pip install --upgrade tox
 
       - name: Unit tests
-        run: |
-          tox -e ci-py -- -v --color=yes
+        if: "!startsWith(matrix.python-version, 'pypy')"
+        run:
+          tox -e ci-py$(echo ${{ matrix.python-version }} | tr -d '.') -- -v --color=yes
 
-      - name: Publish coverage to Coveralls
-        # If pushed / is a pull request against main repo AND
+      - name: Unit tests (pypy)
+        if: "startsWith(matrix.python-version, 'pypy')"
+        run: tox -e ci-pypy3 -- -v --color=yes
+
+      - name: Upload coverage to Coveralls
+        # Upload coverage if we are on the main repository and
         # we're running on Linux (this action only supports Linux)
         if:
-          ((github.event_name == 'push' && github.repository == 'psf/black') ||
-          github.event.pull_request.base.repo.full_name == 'psf/black') && matrix.os ==
-          'ubuntu-latest'
-
-        uses: AndreMiras/coveralls-python-action@v20201129
+          github.repository == 'psf/black' && matrix.os == 'ubuntu-latest' &&
+          !startsWith(matrix.python-version, 'pypy')
+        uses: AndreMiras/coveralls-python-action@8799c9f4443ac4201d2e2f2c725d577174683b99
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
           parallel: true
@@ -60,17 +70,40 @@ jobs:
           debug: true
 
   coveralls-finish:
-    needs: build
-    # If pushed / is a pull request against main repo
-    if:
-      (github.event_name == 'push' && github.repository == 'psf/black') ||
-      github.event.pull_request.base.repo.full_name == 'psf/black'
+    needs: main
+    if: github.repository == 'psf/black'
 
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - name: Coveralls finished
-        uses: AndreMiras/coveralls-python-action@v20201129
+      - uses: actions/checkout@v4
+      - name: Send finished signal to Coveralls
+        uses: AndreMiras/coveralls-python-action@8799c9f4443ac4201d2e2f2c725d577174683b99
         with:
           parallel-finished: true
           debug: true
+
+  uvloop:
+    if:
+      github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
+      github.repository
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, macOS-latest]
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up latest Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "*"
+
+      - name: Install black with uvloop
+        run: |
+          python -m pip install pip --upgrade --disable-pip-version-check
+          python -m pip install -e ".[uvloop]"
+
+      - name: Format ourselves
+        run: python -m black --check .
index 8f44d4ec27b5eaa90102102c9fbda1333a66d4c4..bb19d48158c25e285d3c33d64af598a8a8edae2d 100644 (file)
@@ -1,9 +1,12 @@
-name: Upload self-contained binaries
+name: Publish executables
 
 on:
   release:
     types: [published]
 
+permissions:
+  contents: write # actions/upload-release-asset needs this.
+
 jobs:
   build:
     runs-on: ${{ matrix.os }}
@@ -16,37 +19,38 @@ jobs:
             pathsep: ";"
             asset_name: black_windows.exe
             executable_mime: "application/vnd.microsoft.portable-executable"
-            platform: windows
           - os: ubuntu-20.04
             pathsep: ":"
             asset_name: black_linux
             executable_mime: "application/x-executable"
-            platform: unix
           - os: macos-latest
             pathsep: ":"
             asset_name: black_macos
             executable_mime: "application/x-mach-binary"
-            platform: macos
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
       - name: Set up latest Python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: "*"
 
-      - name: Install dependencies
+      - name: Install Black and PyInstaller
         run: |
-          python -m pip install --upgrade pip wheel setuptools
-          python -m pip install .
+          python -m pip install --upgrade pip wheel
+          python -m pip install .[colorama]
           python -m pip install pyinstaller
 
-      - name: Build binary
+      - name: Build executable with PyInstaller
         run: >
           python -m PyInstaller -F --name ${{ matrix.asset_name }} --add-data
-          'src/blib2to3${{ matrix.pathsep }}blib2to3' --hidden-import platformdirs.${{
-          matrix.platform }} src/black/__main__.py
+          'src/blib2to3${{ matrix.pathsep }}blib2to3' src/black/__main__.py
+
+      - name: Quickly test executable
+        run: |
+          ./dist/${{ matrix.asset_name }} --version
+          ./dist/${{ matrix.asset_name }} src --verbose
 
       - name: Upload binary as release asset
         uses: actions/upload-release-asset@v1
diff --git a/.vim/bundle/black/.github/workflows/uvloop_test.yml b/.vim/bundle/black/.github/workflows/uvloop_test.yml
deleted file mode 100644 (file)
index 5d23ec6..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-name: test uvloop
-
-on:
-  push:
-    paths-ignore:
-      - "docs/**"
-      - "*.md"
-
-  pull_request:
-    paths-ignore:
-      - "docs/**"
-      - "*.md"
-
-jobs:
-  build:
-    # We want to run on external PRs, but not on our own internal PRs as they'll be run
-    # by the push to the branch. Without this if check, checks are duplicated since
-    # internal PRs match both the push and pull_request events.
-    if:
-      github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
-      github.repository
-
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        os: [ubuntu-latest, macOS-latest]
-
-    steps:
-      - uses: actions/checkout@v2
-
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
-
-      - name: Install latest pip
-        run: |
-          python -m pip install --upgrade pip
-
-      - name: Test uvloop Extra Install
-        run: |
-          python -m pip install -e ".[uvloop]"
-
-      - name: Primer uvloop run
-        run: |
-          black-primer
index f81bce8fd4e1117e092851aa0ec327decc5d5dff..4a4f1b738ad12d59e0eb37475abc05c3673b9a0e 100644 (file)
@@ -4,16 +4,22 @@
 _build
 .DS_Store
 .vscode
+.python-version
 docs/_static/pypi.svg
 .tox
 __pycache__
+
+# Packaging artifacts
 black.egg-info
+black.dist-info
 build/
 dist/
 pip-wheel-metadata/
+.eggs
+
 src/_black_version.py
 .idea
-.eggs
+
 .dmypy.json
 *.swp
 .hypothesis/
index a3cd66393848aff55adbe3a67c69b231df9966de..623e661ac0747bdcfaba1709524c5e6a82925a13 100644 (file)
@@ -1,17 +1,9 @@
 # Note: don't use this config for your own repositories. Instead, see
 # "Version control integration" in docs/integrations/source_version_control.md
-exclude: ^(src/blib2to3/|profiling/|tests/data/)
+exclude: ^(profiling/|tests/data/)
 repos:
   - repo: local
     hooks:
-      - id: black
-        name: black
-        language: system
-        entry: black
-        minimum_pre_commit_version: 2.9.2
-        require_serial: true
-        types_or: [python, pyi]
-
       - id: check-pre-commit-rev-in-example
         name: Check pre-commit rev in example
         language: python
@@ -20,7 +12,7 @@ repos:
         additional_dependencies:
           &version_check_dependencies [
             commonmark==0.9.1,
-            pyyaml==5.4.1,
+            pyyaml==6.0.1,
             beautifulsoup4==4.9.3,
           ]
 
@@ -31,33 +23,51 @@ repos:
         files: '(CHANGES\.md|the_basics\.md)$'
         additional_dependencies: *version_check_dependencies
 
-  - repo: https://gitlab.com/pycqa/flake8
-    rev: 3.9.2
+  - repo: https://github.com/pycqa/isort
+    rev: 5.12.0
+    hooks:
+      - id: isort
+
+  - repo: https://github.com/pycqa/flake8
+    rev: 6.1.0
     hooks:
       - id: flake8
-        additional_dependencies: [flake8-bugbear]
+        additional_dependencies:
+          - flake8-bugbear
+          - flake8-comprehensions
+          - flake8-simplify
+        exclude: ^src/blib2to3/
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v0.910
+    rev: v1.5.1
     hooks:
       - id: mypy
         exclude: ^docs/conf.py
+        args: ["--config-file", "pyproject.toml"]
         additional_dependencies:
-          - types-dataclasses >= 0.1.3
           - types-PyYAML
           - tomli >= 0.2.6, < 2.0.0
-          - types-typed-ast >= 1.4.1
-          - click >= 8.0.0
+          - click >= 8.1.0, != 8.1.4, != 8.1.5
+          - packaging >= 22.0
           - platformdirs >= 2.1.0
+          - pytest
+          - hypothesis
+          - aiohttp >= 3.7.4
+          - types-commonmark
+          - urllib3
+          - hypothesmith
 
   - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: v2.3.2
+    rev: v3.0.3
     hooks:
       - id: prettier
-        exclude: ^Pipfile\.lock
+        exclude: \.github/workflows/diff_shades\.yml
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.0.1
+    rev: v4.4.0
     hooks:
       - id: end-of-file-fixer
       - id: trailing-whitespace
+
+ci:
+  autoupdate_schedule: quarterly
index 137957045a6c938eec1408ca9af8ba47d59f4caa..a1ff41fded87d199692f1b68119c5bb390bbab5c 100644 (file)
@@ -1,3 +1,5 @@
+# Note that we recommend using https://github.com/psf/black-pre-commit-mirror instead
+# This will work about 2x as fast as using the hooks in this repository
 - id: black
   name: black
   description: "Black: The uncompromising Python code formatter"
index 24eb3eaf6d9601a0072b2f4f4a5e308516692da3..fa612668850c6cbe18dc94287616ed415bb8b945 100644 (file)
@@ -3,8 +3,12 @@ version: 2
 formats:
   - htmlzip
 
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+
 python:
-  version: 3.8
   install:
     - requirements: docs/requirements.txt
 
index 8d112ea679587ac68f19922686b555e4af8b47d9..e0511bb9b7ca8db6ba56205b28c2afdd07d6f370 100644 (file)
@@ -2,27 +2,36 @@
 
 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),
-[Cooper Lees](mailto:me@cooperlees.com), and Richard Si.
+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)
+- [Cooper Lees](mailto:me@cooperlees.com)
+- [Richard Si](mailto:sichard26@gmail.com)
+- [Felix Hildén](mailto:felix.hilden@gmail.com)
+- [Batuhan Taskaya](mailto:batuhan@python.org)
+- [Shantanu Jain](mailto:hauntsaninja@gmail.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)
+- [Alexander Huynh](mailto:ahrex-gh-psf-black@e.sc)
+- [Alexandr Artemyev](mailto:mogost@gmail.com)
 - [Alex Vandiver](mailto:github@chmrr.net)
 - [Allan Simon](mailto:allan.simon@supinfo.com)
 - Anders-Petter Ljungquist
+- [Amethyst Reese](mailto:amy@n7.gg)
 - [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)
+- [Antonio Ossa Guerra](mailto:aaossa+black@uc.cl)
 - [Arjaan Buijk](mailto:arjaan.buijk@gmail.com)
 - [Arnav Borbornah](mailto:arnavborborah11@gmail.com)
 - [Artem Malyshev](mailto:proofit404@gmail.com)
@@ -73,6 +82,7 @@ Multiple contributions by:
 - [Hugo Barrera](mailto::hugo@barrera.io)
 - Hugo van Kemenade
 - [Hynek Schlawack](mailto:hs@ox.cx)
+- [Ionite](mailto:dev@ionite.io)
 - [Ivan Katanić](mailto:ivan.katanic@gmail.com)
 - [Jakub Kadlubiec](mailto:jakub.kadlubiec@skyscanner.net)
 - [Jakub Warczarek](mailto:jakub.warczarek@gmail.com)
@@ -143,6 +153,7 @@ Multiple contributions by:
 - [Rishikesh Jha](mailto:rishijha424@gmail.com)
 - [Rupert Bedford](mailto:rupert@rupertb.com)
 - Russell Davis
+- [Sagi Shadur](mailto:saroad2@gmail.com)
 - [Rémi Verschelde](mailto:rverschelde@gmail.com)
 - [Sami Salonen](mailto:sakki@iki.fi)
 - [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com)
index 864f0a54410b33cadeaa8ee9b2a5d3817aa9c9d1..79b5c6034e8ad2044f1417a5ccd1c0f4ee1cdf25 100644 (file)
 
 ## Unreleased
 
+### Highlights
+
+<!-- Include any especially major or disruptive changes here -->
+
+### Stable style
+
+<!-- Changes that affect Black's stable style -->
+
+### Preview style
+
+- Fix merging implicit multiline strings that have inline comments (#3956)
+
+### Configuration
+
+<!-- Changes to how Black can be configured -->
+
+### Packaging
+
+<!-- Changes to how Black is packaged, such as dependency requirements -->
+
+### Parser
+
+<!-- Changes to the parser or to version autodetection -->
+
+### Performance
+
+<!-- Changes that improve Black's performance. -->
+
+### Output
+
+<!-- Changes to Black's terminal output and error messages -->
+
+### _Blackd_
+
+<!-- Changes to blackd -->
+
+### Integrations
+
+<!-- For example, Docker, GitHub Actions, pre-commit, editors -->
+
+### Documentation
+
+<!-- Major changes to documentation and policies. Small docs changes
+     don't need a changelog entry. -->
+
+## 23.10.0
+
+### Stable style
+
+- Fix comments getting removed from inside parenthesized strings (#3909)
+
+### Preview style
+
+- Fix long lines with power operators getting split before the line length (#3942)
+- Long type hints are now wrapped in parentheses and properly indented when split across
+  multiple lines (#3899)
+- Magic trailing commas are now respected in return types. (#3916)
+- Require one empty line after module-level docstrings. (#3932)
+- Treat raw triple-quoted strings as docstrings (#3947)
+
+### Configuration
+
+- Fix cache versioning logic when `BLACK_CACHE_DIR` is set (#3937)
+
+### Parser
+
+- Fix bug where attributes named `type` were not acccepted inside `match` statements
+  (#3950)
+- Add support for PEP 695 type aliases containing lambdas and other unusual expressions
+  (#3949)
+
+### Output
+
+- Black no longer attempts to provide special errors for attempting to format Python 2
+  code (#3933)
+- Black will more consistently print stacktraces on internal errors in verbose mode
+  (#3938)
+
+### Integrations
+
+- The action output displayed in the job summary is now wrapped in Markdown (#3914)
+
+## 23.9.1
+
+Due to various issues, the previous release (23.9.0) did not include compiled mypyc
+wheels, which make Black significantly faster. These issues have now been fixed, and
+this release should come with compiled wheels once again.
+
+There will be no wheels for Python 3.12 due to a bug in mypyc. We will provide 3.12
+wheels in a future release as soon as the mypyc bug is fixed.
+
+### Packaging
+
+- Upgrade to mypy 1.5.1 (#3864)
+
+### Performance
+
+- Store raw tuples instead of NamedTuples in Black's cache, improving performance and
+  decreasing the size of the cache (#3877)
+
+## 23.9.0
+
+### Preview style
+
+- More concise formatting for dummy implementations (#3796)
+- In stub files, add a blank line between a statement with a body (e.g an
+  `if sys.version_info > (3, x):`) and a function definition on the same level (#3862)
+- Fix a bug whereby spaces were removed from walrus operators within subscript(#3823)
+
+### Configuration
+
+- Black now applies exclusion and ignore logic before resolving symlinks (#3846)
+
+### Performance
+
+- Avoid importing `IPython` if notebook cells do not contain magics (#3782)
+- Improve caching by comparing file hashes as fallback for mtime and size (#3821)
+
+### _Blackd_
+
+- Fix an issue in `blackd` with single character input (#3558)
+
+### Integrations
+
+- Black now has an
+  [official pre-commit mirror](https://github.com/psf/black-pre-commit-mirror). Swapping
+  `https://github.com/psf/black` to `https://github.com/psf/black-pre-commit-mirror` in
+  your `.pre-commit-config.yaml` will make Black about 2x faster (#3828)
+- The `.black.env` folder specified by `ENV_PATH` will now be removed on the completion
+  of the GitHub Action (#3759)
+
+## 23.7.0
+
+### Highlights
+
+- Runtime support for Python 3.7 has been removed. Formatting 3.7 code will still be
+  supported until further notice (#3765)
+
+### Stable style
+
+- Fix a bug where an illegal trailing comma was added to return type annotations using
+  PEP 604 unions (#3735)
+- Fix several bugs and crashes where comments in stub files were removed or mishandled
+  under some circumstances (#3745)
+- Fix a crash with multi-line magic comments like `type: ignore` within parentheses
+  (#3740)
+- Fix error in AST validation when _Black_ removes trailing whitespace in a type comment
+  (#3773)
+
+### Preview style
+
+- Implicitly concatenated strings used as function args are no longer wrapped inside
+  parentheses (#3640)
+- Remove blank lines between a class definition and its docstring (#3692)
+
+### Configuration
+
+- The `--workers` argument to _Black_ can now be specified via the `BLACK_NUM_WORKERS`
+  environment variable (#3743)
+- `.pytest_cache`, `.ruff_cache` and `.vscode` are now excluded by default (#3691)
+- Fix _Black_ not honouring `pyproject.toml` settings when running `--stdin-filename`
+  and the `pyproject.toml` found isn't in the current working directory (#3719)
+- _Black_ will now error if `exclude` and `extend-exclude` have invalid data types in
+  `pyproject.toml`, instead of silently doing the wrong thing (#3764)
+
+### Packaging
+
+- Upgrade mypyc from 0.991 to 1.3 (#3697)
+- Remove patching of Click that mitigated errors on Python 3.6 with `LANG=C` (#3768)
+
+### Parser
+
+- Add support for the new PEP 695 syntax in Python 3.12 (#3703)
+
+### Performance
+
+- Speed up _Black_ significantly when the cache is full (#3751)
+- Avoid importing `IPython` in a case where we wouldn't need it (#3748)
+
+### Output
+
+- Use aware UTC datetimes internally, avoids deprecation warning on Python 3.12 (#3728)
+- Change verbose logging to exactly mirror _Black_'s logic for source discovery (#3749)
+
+### _Blackd_
+
+- The `blackd` argument parser now shows the default values for options in their help
+  text (#3712)
+
+### Integrations
+
+- Black is now tested with
+  [`PYTHONWARNDEFAULTENCODING = 1`](https://docs.python.org/3/library/io.html#io-encoding-warning)
+  (#3763)
+- Update GitHub Action to display black output in the job summary (#3688)
+
+### Documentation
+
+- Add a CITATION.cff file to the root of the repository, containing metadata on how to
+  cite this software (#3723)
+- Update the _classes_ and _exceptions_ documentation in Developer reference to match
+  the latest code base (#3755)
+
+## 23.3.0
+
+### Highlights
+
+This release fixes a longstanding confusing behavior in Black's GitHub action, where the
+version of the action did not determine the version of Black being run (issue #3382). In
+addition, there is a small bug fix around imports and a number of improvements to the
+preview style.
+
+Please try out the
+[preview style](https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html#preview-style)
+with `black --preview` and tell us your feedback. All changes in the preview style are
+expected to become part of Black's stable style in January 2024.
+
+### Stable style
+
+- Import lines with `# fmt: skip` and `# fmt: off` no longer have an extra blank line
+  added when they are right after another import line (#3610)
+
+### Preview style
+
+- Add trailing commas to collection literals even if there's a comment after the last
+  entry (#3393)
+- `async def`, `async for`, and `async with` statements are now formatted consistently
+  compared to their non-async version. (#3609)
+- `with` statements that contain two context managers will be consistently wrapped in
+  parentheses (#3589)
+- Let string splitters respect [East Asian Width](https://www.unicode.org/reports/tr11/)
+  (#3445)
+- Now long string literals can be split after East Asian commas and periods (`、` U+3001
+  IDEOGRAPHIC COMMA, `。` U+3002 IDEOGRAPHIC FULL STOP, & `,` U+FF0C FULLWIDTH COMMA)
+  besides before spaces (#3445)
+- For stubs, enforce one blank line after a nested class with a body other than just
+  `...` (#3564)
+- Improve handling of multiline strings by changing line split behavior (#1879)
+
+### Parser
+
+- Added support for formatting files with invalid type comments (#3594)
+
+### Integrations
+
+- Update GitHub Action to use the version of Black equivalent to action's version if
+  version input is not specified (#3543)
+- Fix missing Python binary path in autoload script for vim (#3508)
+
+### Documentation
+
+- Document that only the most recent release is supported for security issues;
+  vulnerabilities should be reported through Tidelift (#3612)
+
+## 23.1.0
+
+### Highlights
+
+This is the first release of 2023, and following our
+[stability policy](https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy),
+it comes with a number of improvements to our stable style, including improvements to
+empty line handling, removal of redundant parentheses in several contexts, and output
+that highlights implicitly concatenated strings better.
+
+There are also many changes to the preview style; try out `black --preview` and give us
+feedback to help us set the stable style for next year.
+
+In addition to style changes, Black now automatically infers the supported Python
+versions from your `pyproject.toml` file, removing the need to set Black's target
+versions separately.
+
+### Stable style
+
+<!-- Changes that affect Black's stable style -->
+
+- Introduce the 2023 stable style, which incorporates most aspects of last year's
+  preview style (#3418). Specific changes:
+  - Enforce empty lines before classes and functions with sticky leading comments
+    (#3302) (22.12.0)
+  - Reformat empty and whitespace-only files as either an empty file (if no newline is
+    present) or as a single newline character (if a newline is present) (#3348)
+    (22.12.0)
+  - Implicitly concatenated strings used as function args are now wrapped inside
+    parentheses (#3307) (22.12.0)
+  - Correctly handle trailing commas that are inside a line's leading non-nested parens
+    (#3370) (22.12.0)
+  - `--skip-string-normalization` / `-S` now prevents docstring prefixes from being
+    normalized as expected (#3168) (since 22.8.0)
+  - When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from
+    subscript expressions with more than 1 element (#3209) (22.8.0)
+  - Implicitly concatenated strings inside a list, set, or tuple are now wrapped inside
+    parentheses (#3162) (22.8.0)
+  - Fix a string merging/split issue when a comment is present in the middle of
+    implicitly concatenated strings on its own line (#3227) (22.8.0)
+  - Docstring quotes are no longer moved if it would violate the line length limit
+    (#3044, #3430) (22.6.0)
+  - Parentheses around return annotations are now managed (#2990) (22.6.0)
+  - Remove unnecessary parentheses around awaited objects (#2991) (22.6.0)
+  - Remove unnecessary parentheses in `with` statements (#2926) (22.6.0)
+  - Remove trailing newlines after code block open (#3035) (22.6.0)
+  - Code cell separators `#%%` are now standardised to `# %%` (#2919) (22.3.0)
+  - Remove unnecessary parentheses from `except` statements (#2939) (22.3.0)
+  - Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945) (22.3.0)
+  - Avoid magic-trailing-comma in single-element subscripts (#2942) (22.3.0)
+- Fix a crash when a colon line is marked between `# fmt: off` and `# fmt: on` (#3439)
+
+### Preview style
+
+<!-- Changes that affect Black's preview style -->
+
+- Format hex codes in unicode escape sequences in string literals (#2916)
+- Add parentheses around `if`-`else` expressions (#2278)
+- Improve performance on large expressions that contain many strings (#3467)
+- Fix a crash in preview style with assert + parenthesized string (#3415)
+- Fix crashes in preview style with walrus operators used in function return annotations
+  and except clauses (#3423)
+- Fix a crash in preview advanced string processing where mixed implicitly concatenated
+  regular and f-strings start with an empty span (#3463)
+- Fix a crash in preview advanced string processing where a standalone comment is placed
+  before a dict's value (#3469)
+- Fix an issue where extra empty lines are added when a decorator has `# fmt: skip`
+  applied or there is a standalone comment between decorators (#3470)
+- Do not put the closing quotes in a docstring on a separate line, even if the line is
+  too long (#3430)
+- Long values in dict literals are now wrapped in parentheses; correspondingly
+  unnecessary parentheses around short values in dict literals are now removed; long
+  string lambda values are now wrapped in parentheses (#3440)
+- Fix two crashes in preview style involving edge cases with docstrings (#3451)
+- Exclude string type annotations from improved string processing; fix crash when the
+  return type annotation is stringified and spans across multiple lines (#3462)
+- Wrap multiple context managers in parentheses when targeting Python 3.9+ (#3489)
+- Fix several crashes in preview style with walrus operators used in `with` statements
+  or tuples (#3473)
+- Fix an invalid quote escaping bug in f-string expressions where it produced invalid
+  code. Implicitly concatenated f-strings with different quotes can now be merged or
+  quote-normalized by changing the quotes used in expressions. (#3509)
+- Fix crash on `await (yield)` when Black is compiled with mypyc (#3533)
+
+### Configuration
+
+<!-- Changes to how Black can be configured -->
+
+- Black now tries to infer its `--target-version` from the project metadata specified in
+  `pyproject.toml` (#3219)
+
+### Packaging
+
+<!-- Changes to how Black is packaged, such as dependency requirements -->
+
+- Upgrade mypyc from `0.971` to `0.991` so mypycified _Black_ can be built on armv7
+  (#3380)
+  - This also fixes some crashes while using compiled Black with a debug build of
+    CPython
+- Drop specific support for the `tomli` requirement on 3.11 alpha releases, working
+  around a bug that would cause the requirement not to be installed on any non-final
+  Python releases (#3448)
+- Black now depends on `packaging` version `22.0` or later. This is required for new
+  functionality that needs to parse part of the project metadata (#3219)
+
+### Output
+
+<!-- Changes to Black's terminal output and error messages -->
+
+- Calling `black --help` multiple times will return the same help contents each time
+  (#3516)
+- Verbose logging now shows the values of `pyproject.toml` configuration variables
+  (#3392)
+- Fix false symlink detection messages in verbose output due to using an incorrect
+  relative path to the project root (#3385)
+
+### Integrations
+
+<!-- For example, Docker, GitHub Actions, pre-commit, editors -->
+
+- Move 3.11 CI to normal flow now that all dependencies support 3.11 (#3446)
+- Docker: Add new `latest_prerelease` tag automation to follow latest black alpha
+  release on docker images (#3465)
+
+### Documentation
+
+<!-- Major changes to documentation and policies. Small docs changes
+     don't need a changelog entry. -->
+
+- Expand `vim-plug` installation instructions to offer more explicit options (#3468)
+
+## 22.12.0
+
+### Preview style
+
+<!-- Changes that affect Black's preview style -->
+
+- Enforce empty lines before classes and functions with sticky leading comments (#3302)
+- Reformat empty and whitespace-only files as either an empty file (if no newline is
+  present) or as a single newline character (if a newline is present) (#3348)
+- Implicitly concatenated strings used as function args are now wrapped inside
+  parentheses (#3307)
+- For assignment statements, prefer splitting the right hand side if the left hand side
+  fits on a single line (#3368)
+- Correctly handle trailing commas that are inside a line's leading non-nested parens
+  (#3370)
+
+### Configuration
+
+<!-- Changes to how Black can be configured -->
+
+- Fix incorrectly applied `.gitignore` rules by considering the `.gitignore` location
+  and the relative path to the target file (#3338)
+- Fix incorrectly ignoring `.gitignore` presence when more than one source directory is
+  specified (#3336)
+
+### Parser
+
+<!-- Changes to the parser or to version autodetection -->
+
+- Parsing support has been added for walruses inside generator expression that are
+  passed as function args (for example,
+  `any(match := my_re.match(text) for text in texts)`) (#3327).
+
+### Integrations
+
+<!-- For example, Docker, GitHub Actions, pre-commit, editors -->
+
+- Vim plugin: Optionally allow using the system installation of Black via
+  `let g:black_use_virtualenv = 0`(#3309)
+
+## 22.10.0
+
+### Highlights
+
+- Runtime support for Python 3.6 has been removed. Formatting 3.6 code will still be
+  supported until further notice.
+
+### Stable style
+
+- Fix a crash when `# fmt: on` is used on a different block level than `# fmt: off`
+  (#3281)
+
+### Preview style
+
+- Fix a crash when formatting some dicts with parenthesis-wrapped long string keys
+  (#3262)
+
+### Configuration
+
+- `.ipynb_checkpoints` directories are now excluded by default (#3293)
+- Add `--skip-source-first-line` / `-x` option to ignore the first line of source code
+  while formatting (#3299)
+
+### Packaging
+
+- Executables made with PyInstaller will no longer crash when formatting several files
+  at once on macOS. Native x86-64 executables for macOS are available once again.
+  (#3275)
+- Hatchling is now used as the build backend. This will not have any effect for users
+  who install Black with its wheels from PyPI. (#3233)
+- Faster compiled wheels are now available for CPython 3.11 (#3276)
+
+### _Blackd_
+
+- Windows style (CRLF) newlines will be preserved (#3257).
+
+### Integrations
+
+- Vim plugin: add flag (`g:black_preview`) to enable/disable the preview style (#3246)
+- Update GitHub Action to support formatting of Jupyter Notebook files via a `jupyter`
+  option (#3282)
+- Update GitHub Action to support use of version specifiers (e.g. `<23`) for Black
+  version (#3265)
+
+## 22.8.0
+
+### Highlights
+
+- Python 3.11 is now supported, except for _blackd_ as aiohttp does not support 3.11 as
+  of publishing (#3234)
+- This is the last release that supports running _Black_ on Python 3.6 (formatting 3.6
+  code will continue to be supported until further notice)
+- Reword the stability policy to say that we may, in rare cases, make changes that
+  affect code that was not previously formatted by _Black_ (#3155)
+
+### Stable style
+
+- Fix an infinite loop when using `# fmt: on/off` in the middle of an expression or code
+  block (#3158)
+- Fix incorrect handling of `# fmt: skip` on colon (`:`) lines (#3148)
+- Comments are no longer deleted when a line had spaces removed around power operators
+  (#2874)
+
+### Preview style
+
+- Single-character closing docstring quotes are no longer moved to their own line as
+  this is invalid. This was a bug introduced in version 22.6.0. (#3166)
+- `--skip-string-normalization` / `-S` now prevents docstring prefixes from being
+  normalized as expected (#3168)
+- When using `--skip-magic-trailing-comma` or `-C`, trailing commas are stripped from
+  subscript expressions with more than 1 element (#3209)
+- Implicitly concatenated strings inside a list, set, or tuple are now wrapped inside
+  parentheses (#3162)
+- Fix a string merging/split issue when a comment is present in the middle of implicitly
+  concatenated strings on its own line (#3227)
+
+### _Blackd_
+
+- `blackd` now supports enabling the preview style via the `X-Preview` header (#3217)
+
+### Configuration
+
+- Black now uses the presence of debug f-strings to detect target version (#3215)
+- Fix misdetection of project root and verbose logging of sources in cases involving
+  `--stdin-filename` (#3216)
+- Immediate `.gitignore` files in source directories given on the command line are now
+  also respected, previously only `.gitignore` files in the project root and
+  automatically discovered directories were respected (#3237)
+
+### Documentation
+
+- Recommend using BlackConnect in IntelliJ IDEs (#3150)
+
+### Integrations
+
+- Vim plugin: prefix messages with `Black: ` so it's clear they come from Black (#3194)
+- Docker: changed to a /opt/venv installation + added to PATH to be available to
+  non-root users (#3202)
+
+### Output
+
+- Change from deprecated `asyncio.get_event_loop()` to create our event loop which
+  removes DeprecationWarning (#3164)
+- Remove logging from internal `blib2to3` library since it regularly emits error logs
+  about failed caching that can and should be ignored (#3193)
+
+### Parser
+
+- Type comments are now included in the AST equivalence check consistently so accidental
+  deletion raises an error. Though type comments can't be tracked when running on PyPy
+  3.7 due to standard library limitations. (#2874)
+
+### Performance
+
+- Reduce Black's startup time when formatting a single file by 15-30% (#3211)
+
+## 22.6.0
+
+### Style
+
+- Fix unstable formatting involving `#fmt: skip` and `# fmt:skip` comments (notice the
+  lack of spaces) (#2970)
+
+### Preview style
+
+- Docstring quotes are no longer moved if it would violate the line length limit (#3044)
+- Parentheses around return annotations are now managed (#2990)
+- Remove unnecessary parentheses around awaited objects (#2991)
+- Remove unnecessary parentheses in `with` statements (#2926)
+- Remove trailing newlines after code block open (#3035)
+
+### Integrations
+
+- Add `scripts/migrate-black.py` script to ease introduction of Black to a Git project
+  (#3038)
+
+### Output
+
+- Output Python version and implementation as part of `--version` flag (#2997)
+
+### Packaging
+
+- Use `tomli` instead of `tomllib` on Python 3.11 builds where `tomllib` is not
+  available (#2987)
+
+### Parser
+
+- [PEP 654](https://peps.python.org/pep-0654/#except) syntax (for example,
+  `except *ExceptionGroup:`) is now supported (#3016)
+- [PEP 646](https://peps.python.org/pep-0646) syntax (for example,
+  `Array[Batch, *Shape]` or `def fn(*args: *T) -> None`) is now supported (#3071)
+
+### Vim Plugin
+
+- Fix `strtobool` function. It didn't parse true/on/false/off. (#3025)
+
+## 22.3.0
+
+### Preview style
+
+- Code cell separators `#%%` are now standardised to `# %%` (#2919)
+- Remove unnecessary parentheses from `except` statements (#2939)
+- Remove unnecessary parentheses from tuple unpacking in `for` loops (#2945)
+- Avoid magic-trailing-comma in single-element subscripts (#2942)
+
+### Configuration
+
+- Do not format `__pypackages__` directories by default (#2836)
+- Add support for specifying stable version with `--required-version` (#2832).
+- Avoid crashing when the user has no homedir (#2814)
+- Avoid crashing when md5 is not available (#2905)
+- Fix handling of directory junctions on Windows (#2904)
+
+### Documentation
+
+- Update pylint config documentation (#2931)
+
+### Integrations
+
+- Move test to disable plugin in Vim/Neovim, which speeds up loading (#2896)
+
+### Output
+
+- In verbose mode, log when _Black_ is using user-level config (#2861)
+
+### Packaging
+
+- Fix Black to work with Click 8.1.0 (#2966)
+- On Python 3.11 and newer, use the standard library's `tomllib` instead of `tomli`
+  (#2903)
+- `black-primer`, the deprecated internal devtool, has been removed and copied to a
+  [separate repository](https://github.com/cooperlees/black-primer) (#2924)
+
+### Parser
+
+- Black can now parse starred expressions in the target of `for` and `async for`
+  statements, e.g `for item in *items_1, *items_2: pass` (#2879).
+
+## 22.1.0
+
+At long last, _Black_ is no longer a beta product! This is the first non-beta release
+and the first release covered by our new
+[stability policy](https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy).
+
+### Highlights
+
+- **Remove Python 2 support** (#2740)
+- Introduce the `--preview` flag (#2752)
+
+### Style
+
+- Deprecate `--experimental-string-processing` and move the functionality under
+  `--preview` (#2789)
+- For stubs, one blank line between class attributes and methods is now kept if there's
+  at least one pre-existing blank line (#2736)
+- Black now normalizes string prefix order (#2297)
+- Remove spaces around power operators if both operands are simple (#2726)
+- Work around bug that causes unstable formatting in some cases in the presence of the
+  magic trailing comma (#2807)
+- Use parentheses for attribute access on decimal float and int literals (#2799)
+- Don't add whitespace for attribute access on hexadecimal, binary, octal, and complex
+  literals (#2799)
+- Treat blank lines in stubs the same inside top-level `if` statements (#2820)
+- Fix unstable formatting with semicolons and arithmetic expressions (#2817)
+- Fix unstable formatting around magic trailing comma (#2572)
+
+### Parser
+
+- Fix mapping cases that contain as-expressions, like `case {"key": 1 | 2 as password}`
+  (#2686)
+- Fix cases that contain multiple top-level as-expressions, like `case 1 as a, 2 as b`
+  (#2716)
+- Fix call patterns that contain as-expressions with keyword arguments, like
+  `case Foo(bar=baz as quux)` (#2749)
+- Tuple unpacking on `return` and `yield` constructs now implies 3.8+ (#2700)
+- Unparenthesized tuples on annotated assignments (e.g
+  `values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708)
+- Fix handling of standalone `match()` or `case()` when there is a trailing newline or a
+  comment inside of the parentheses. (#2760)
+- `from __future__ import annotations` statement now implies Python 3.7+ (#2690)
+
+### Performance
+
+- Speed-up the new backtracking parser about 4X in general (enabled when
+  `--target-version` is set to 3.10 and higher). (#2728)
+- _Black_ is now compiled with [mypyc](https://github.com/mypyc/mypyc) for an overall 2x
+  speed-up. 64-bit Windows, MacOS, and Linux (not including musl) are supported. (#1009,
+  #2431)
+
+### Configuration
+
+- Do not accept bare carriage return line endings in pyproject.toml (#2408)
+- Add configuration option (`python-cell-magics`) to format cells with custom magics in
+  Jupyter Notebooks (#2744)
+- Allow setting custom cache directory on all platforms with environment variable
+  `BLACK_CACHE_DIR` (#2739).
+- Enable Python 3.10+ by default, without any extra need to specify
+  `--target-version=py310`. (#2758)
+- Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804)
+
+### Output
+
+- Improve error message for invalid regular expression (#2678)
+- Improve error message when parsing fails during AST safety check by embedding the
+  underlying SyntaxError (#2693)
+- No longer color diff headers white as it's unreadable in light themed terminals
+  (#2691)
+- Text coloring added in the final statistics (#2712)
+- Verbose mode also now describes how a project root was discovered and which paths will
+  be formatted. (#2526)
+
+### Packaging
+
+- All upper version bounds on dependencies have been removed (#2718)
+- `typing-extensions` is no longer a required dependency in Python 3.10+ (#2772)
+- Set `click` lower bound to `8.0.0` (#2791)
+
+### Integrations
+
+- Update GitHub action to support containerized runs (#2748)
+
+### Documentation
+
+- Change protocol in pip installation instructions to `https://` (#2761)
+- Change HTML theme to Furo primarily for its responsive design and mobile support
+  (#2793)
+- Deprecate the `black-primer` tool (#2809)
+- Document Python support policy (#2819)
+
+## 21.12b0
+
+### _Black_
+
+- Fix determination of f-string expression spans (#2654)
+- Fix bad formatting of error messages about EOF in multi-line statements (#2343)
+- Functions and classes in blocks now have more consistent surrounding spacing (#2472)
+
+#### Jupyter Notebook support
+
+- Cell magics are now only processed if they are known Python cell magics. Earlier, all
+  cell magics were tokenized, leading to possible indentation errors e.g. with
+  `%%writefile`. (#2630)
+- Fix assignment to environment variables in Jupyter Notebooks (#2642)
+
+#### Python 3.10 support
+
+- Point users to using `--target-version py310` if we detect 3.10-only syntax (#2668)
+- Fix `match` statements with open sequence subjects, like `match a, b:` or
+  `match a, *b:` (#2639) (#2659)
+- Fix `match`/`case` statements that contain `match`/`case` soft keywords multiple
+  times, like `match re.match()` (#2661)
+- Fix `case` statements with an inline body (#2665)
+- Fix styling of starred expressions inside `match` subject (#2667)
+- Fix parser error location on invalid syntax in a `match` statement (#2649)
+- Fix Python 3.10 support on platforms without ProcessPoolExecutor (#2631)
+- Improve parsing performance on code that uses `match` under `--target-version py310`
+  up to ~50% (#2670)
+
+### Packaging
+
+- Remove dependency on `regex` (#2644) (#2663)
+
+## 21.11b1
+
+### _Black_
+
+- Bumped regex version minimum to 2021.4.4 to fix Pattern class usage (#2621)
+
+## 21.11b0
+
 ### _Black_
 
+- Warn about Python 2 deprecation in more cases by improving Python 2 only syntax
+  detection (#2592)
+- Add experimental PyPy support (#2559)
+- Add partial support for the match statement. As it's experimental, it's only enabled
+  when `--target-version py310` is explicitly specified (#2586)
+- Add support for parenthesized with (#2586)
+- Declare support for Python 3.10 for running Black (#2562)
+
+### Integrations
+
+- Fixed vim plugin with Python 3.10 by removing deprecated distutils import (#2610)
+- The vim plugin now parses `skip_magic_trailing_comma` from pyproject.toml (#2613)
+
+## 21.10b0
+
+### _Black_
+
+- Document stability policy, that will apply for non-beta releases (#2529)
 - Add new `--workers` parameter (#2514)
 - Fixed feature detection for positional-only arguments in lambdas (#2532)
-- Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519)
+- Bumped typed-ast version minimum to 1.4.3 for 3.10 compatibility (#2519)
+- Fixed a Python 3.10 compatibility issue where the loop argument was still being passed
+  even though it has been removed (#2580)
+- Deprecate Python 2 formatting support (#2523)
 
 ### _Blackd_
 
 - Remove dependency on aiohttp-cors (#2500)
 - Bump required aiohttp version to 3.7.4 (#2509)
 
+### _Black-Primer_
+
+- Add primer support for --projects (#2555)
+- Print primer summary after individual failures (#2570)
+
 ### Integrations
 
 - Allow to pass `target_version` in the vim plugin (#1319)
+- Install build tools in docker file and use multi-stage build to keep the image size
+  down (#2582)
 
 ## 21.9b0
 
diff --git a/.vim/bundle/black/CITATION.cff b/.vim/bundle/black/CITATION.cff
new file mode 100644 (file)
index 0000000..7ff0e3c
--- /dev/null
@@ -0,0 +1,22 @@
+cff-version: 1.2.0
+title: "Black: The uncompromising Python code formatter"
+message: >-
+  If you use this software, please cite it using the metadata from this file.
+type: software
+authors:
+  - family-names: Langa
+    given-names: Łukasz
+  - name: "contributors to Black"
+repository-code: "https://github.com/psf/black"
+url: "https://black.readthedocs.io/en/stable/"
+abstract: >-
+  Black is the uncompromising Python code formatter. By using it, you agree to cede
+  control over minutiae of hand-formatting. In return, Black gives you speed,
+  determinism, and freedom from pycodestyle nagging about formatting. You will save time
+  and mental energy for more important matters.
+
+  Blackened code looks the same regardless of the project you're reading. Formatting
+  becomes transparent after a while and you can focus on the content instead.
+
+  Black makes code review faster by producing the smallest diffs possible.
+license: MIT
index 9542479eca56e9c03ba29d0ac5123ee8e0d3a2c7..a9e0ea5081e1ee3f4c3b53fdd5ad85f683684b53 100644 (file)
@@ -1,14 +1,19 @@
-FROM python:3-slim
+FROM python:3.11-slim AS builder
 
 RUN mkdir /src
 COPY . /src/
-RUN pip install --no-cache-dir --upgrade pip setuptools wheel \
-    && apt update && apt install -y git \
+ENV VIRTUAL_ENV=/opt/venv
+RUN python -m venv $VIRTUAL_ENV
+RUN . /opt/venv/bin/activate && pip install --no-cache-dir --upgrade pip setuptools wheel \
+    # Install build tools to compile dependencies that don't have prebuilt wheels
+    && apt update && apt install -y git build-essential \
     && cd /src \
-    && pip install --no-cache-dir .[colorama,d] \
-    && rm -rf /src \
-    && apt remove -y git \
-    && apt autoremove -y \
-    && rm -rf /var/lib/apt/lists/*
+    && pip install --no-cache-dir .[colorama,d]
 
-CMD ["black"]
+FROM python:3.11-slim
+
+# copy only Python packages to limit the image size
+COPY --from=builder /opt/venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+CMD ["/opt/venv/bin/black"]
diff --git a/.vim/bundle/black/MANIFEST.in b/.vim/bundle/black/MANIFEST.in
deleted file mode 100644 (file)
index 5e53af3..0000000
+++ /dev/null
@@ -1 +0,0 @@
-prune profiling
diff --git a/.vim/bundle/black/Pipfile b/.vim/bundle/black/Pipfile
deleted file mode 100644 (file)
index 90e8f62..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-[[source]]
-name = "pypi"
-url = "https://pypi.python.org/simple"
-verify_ssl = true
-
-[dev-packages]
-# Testing related requirements.
-coverage = ">= 5.3"
-pytest = " >= 6.1.1"
-pytest-xdist = ">= 2.2.1"
-pytest-cov = ">= 2.11.1"
-tox = "*"
-
-# Linting related requirements.
-pre-commit = ">=2.9.2"
-flake8 = ">=3.9.2"
-flake8-bugbear = "*"
-mypy = ">=0.910"
-types-dataclasses = ">=0.1.3"
-types-typed-ast = ">=1.4.1"
-types-PyYAML = ">=5.4.1"
-
-# Documentation related requirements.
-Sphinx = ">=4.1.2"
-MyST-Parser = ">=0.15.1"
-sphinxcontrib-programoutput = ">=0.17"
-sphinx-copybutton = ">=0.4.0"
-docutils = "==0.17.1"  # not a direct dependency, see https://github.com/pypa/pipenv/issues/3865
-
-# Packaging related requirements.
-setuptools = ">=39.2.0"
-setuptools-scm = "*"
-twine = ">=1.11.0"
-wheel = ">=0.31.1"
-readme_renderer = "*"
-
-black = {editable = true, extras = ["d", "jupyter"], path = "."}
-
-[packages]
-aiohttp = ">=3.7.4"
-platformdirs= ">=2"
-click = ">=8.0.0"
-mypy_extensions = ">=0.4.3"
-pathspec = ">=0.8.1"
-regex = ">=2020.1.8"
-tomli = ">=0.2.6, <2.0.0"
-typed-ast = "==1.4.3"
-typing_extensions = {markers = "python_version < '3.10'", version = ">=3.10.0.0"}
-black = {editable = true,extras = ["d"],path = "."}
-dataclasses = {markers = "python_version < '3.7'", version = ">0.1.3"}
diff --git a/.vim/bundle/black/Pipfile.lock b/.vim/bundle/black/Pipfile.lock
deleted file mode 100644 (file)
index 2ddeca8..0000000
+++ /dev/null
@@ -1,1613 +0,0 @@
-{
-    "_meta": {
-        "hash": {
-            "sha256": "4baa020356174f89177af103f1966928e7b9c2a69df3a9d4e8070eb83ee19387"
-        },
-        "pipfile-spec": 6,
-        "requires": {},
-        "sources": [
-            {
-                "name": "pypi",
-                "url": "https://pypi.python.org/simple",
-                "verify_ssl": true
-            }
-        ]
-    },
-    "default": {
-        "aiohttp": {
-            "hashes": [
-                "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe",
-                "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe",
-                "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5",
-                "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8",
-                "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd",
-                "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb",
-                "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c",
-                "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87",
-                "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0",
-                "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290",
-                "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5",
-                "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287",
-                "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde",
-                "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf",
-                "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8",
-                "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16",
-                "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf",
-                "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809",
-                "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213",
-                "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f",
-                "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013",
-                "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b",
-                "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9",
-                "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5",
-                "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb",
-                "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df",
-                "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4",
-                "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439",
-                "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f",
-                "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22",
-                "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f",
-                "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5",
-                "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970",
-                "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009",
-                "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc",
-                "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
-                "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
-            ],
-            "index": "pypi",
-            "version": "==3.7.4.post0"
-        },
-        "async-timeout": {
-            "hashes": [
-                "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
-                "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
-            ],
-            "markers": "python_full_version >= '3.5.3'",
-            "version": "==3.0.1"
-        },
-        "attrs": {
-            "hashes": [
-                "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
-                "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==21.2.0"
-        },
-        "black": {
-            "editable": true,
-            "extras": [
-                "d"
-            ],
-            "path": "."
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
-                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==4.0.0"
-        },
-        "click": {
-            "hashes": [
-                "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
-                "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
-            ],
-            "index": "pypi",
-            "version": "==8.0.1"
-        },
-        "dataclasses": {
-            "hashes": [
-                "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf",
-                "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"
-            ],
-            "index": "pypi",
-            "markers": "python_version < '3.7'",
-            "version": "==0.8"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
-                "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==3.2"
-        },
-        "idna-ssl": {
-            "hashes": [
-                "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
-            ],
-            "markers": "python_version < '3.7'",
-            "version": "==1.1.0"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f",
-                "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"
-            ],
-            "markers": "python_version < '3.8'",
-            "version": "==4.6.4"
-        },
-        "multidict": {
-            "hashes": [
-                "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a",
-                "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93",
-                "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632",
-                "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656",
-                "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79",
-                "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7",
-                "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d",
-                "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5",
-                "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224",
-                "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26",
-                "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea",
-                "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348",
-                "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6",
-                "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76",
-                "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1",
-                "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f",
-                "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952",
-                "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a",
-                "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37",
-                "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9",
-                "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359",
-                "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8",
-                "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da",
-                "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3",
-                "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d",
-                "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf",
-                "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841",
-                "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d",
-                "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93",
-                "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f",
-                "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647",
-                "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635",
-                "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456",
-                "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda",
-                "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5",
-                "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
-                "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==5.1.0"
-        },
-        "mypy-extensions": {
-            "hashes": [
-                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
-                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
-            ],
-            "index": "pypi",
-            "version": "==0.4.3"
-        },
-        "packaging": {
-            "hashes": [
-                "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
-                "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==21.0"
-        },
-        "pathspec": {
-            "hashes": [
-                "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
-                "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
-            ],
-            "index": "pypi",
-            "version": "==0.9.0"
-        },
-        "platformdirs": {
-            "hashes": [
-                "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
-                "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
-            ],
-            "index": "pypi",
-            "version": "==2.2.0"
-        },
-        "pyparsing": {
-            "hashes": [
-                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
-                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
-            ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.4.7"
-        },
-        "regex": {
-            "hashes": [
-                "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd",
-                "sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642",
-                "sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1",
-                "sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321",
-                "sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529",
-                "sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36",
-                "sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a",
-                "sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30",
-                "sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce",
-                "sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376",
-                "sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd",
-                "sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586",
-                "sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7",
-                "sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9",
-                "sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea",
-                "sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94",
-                "sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3",
-                "sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f",
-                "sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267",
-                "sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc",
-                "sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23",
-                "sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882",
-                "sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc",
-                "sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe",
-                "sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759",
-                "sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456",
-                "sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239",
-                "sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb",
-                "sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948",
-                "sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0",
-                "sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183",
-                "sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92",
-                "sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade",
-                "sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044",
-                "sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee",
-                "sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033",
-                "sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2",
-                "sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5",
-                "sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2",
-                "sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504",
-                "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a"
-            ],
-            "index": "pypi",
-            "version": "==2021.8.21"
-        },
-        "setuptools-scm": {
-            "extras": [
-                "toml"
-            ],
-            "hashes": [
-                "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c",
-                "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==6.0.1"
-        },
-        "tomli": {
-            "hashes": [
-                "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f",
-                "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"
-            ],
-            "index": "pypi",
-            "version": "==1.2.1"
-        },
-        "typed-ast": {
-            "hashes": [
-                "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1",
-                "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d",
-                "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6",
-                "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd",
-                "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37",
-                "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151",
-                "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07",
-                "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440",
-                "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70",
-                "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496",
-                "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea",
-                "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400",
-                "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc",
-                "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606",
-                "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc",
-                "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581",
-                "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412",
-                "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a",
-                "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2",
-                "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787",
-                "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f",
-                "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937",
-                "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64",
-                "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487",
-                "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b",
-                "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41",
-                "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a",
-                "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3",
-                "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166",
-                "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"
-            ],
-            "index": "pypi",
-            "version": "==1.4.3"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
-                "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
-                "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
-            ],
-            "index": "pypi",
-            "markers": "python_version < '3.10'",
-            "version": "==3.10.0.0"
-        },
-        "yarl": {
-            "hashes": [
-                "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e",
-                "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434",
-                "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366",
-                "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3",
-                "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec",
-                "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959",
-                "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e",
-                "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c",
-                "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6",
-                "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a",
-                "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6",
-                "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424",
-                "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e",
-                "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f",
-                "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50",
-                "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2",
-                "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc",
-                "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4",
-                "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970",
-                "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10",
-                "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0",
-                "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406",
-                "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896",
-                "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643",
-                "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721",
-                "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478",
-                "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724",
-                "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e",
-                "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8",
-                "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96",
-                "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25",
-                "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76",
-                "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2",
-                "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2",
-                "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c",
-                "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
-                "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.6.3"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
-                "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.5.0"
-        }
-    },
-    "develop": {
-        "aiohttp": {
-            "hashes": [
-                "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe",
-                "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe",
-                "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5",
-                "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8",
-                "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd",
-                "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb",
-                "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c",
-                "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87",
-                "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0",
-                "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290",
-                "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5",
-                "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287",
-                "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde",
-                "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf",
-                "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8",
-                "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16",
-                "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf",
-                "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809",
-                "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213",
-                "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f",
-                "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013",
-                "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b",
-                "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9",
-                "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5",
-                "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb",
-                "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df",
-                "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4",
-                "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439",
-                "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f",
-                "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22",
-                "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f",
-                "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5",
-                "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970",
-                "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009",
-                "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc",
-                "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
-                "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
-            ],
-            "index": "pypi",
-            "version": "==3.7.4.post0"
-        },
-        "alabaster": {
-            "hashes": [
-                "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
-                "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
-            ],
-            "version": "==0.7.12"
-        },
-        "async-timeout": {
-            "hashes": [
-                "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
-                "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
-            ],
-            "markers": "python_full_version >= '3.5.3'",
-            "version": "==3.0.1"
-        },
-        "attrs": {
-            "hashes": [
-                "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
-                "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==21.2.0"
-        },
-        "babel": {
-            "hashes": [
-                "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9",
-                "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.9.1"
-        },
-        "backcall": {
-            "hashes": [
-                "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e",
-                "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"
-            ],
-            "version": "==0.2.0"
-        },
-        "backports.entry-points-selectable": {
-            "hashes": [
-                "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a",
-                "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"
-            ],
-            "markers": "python_version >= '2.7'",
-            "version": "==1.1.0"
-        },
-        "black": {
-            "editable": true,
-            "extras": [
-                "d"
-            ],
-            "path": "."
-        },
-        "bleach": {
-            "hashes": [
-                "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d",
-                "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==4.0.0"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
-                "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
-            ],
-            "version": "==2021.5.30"
-        },
-        "cffi": {
-            "hashes": [
-                "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d",
-                "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771",
-                "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872",
-                "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c",
-                "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc",
-                "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762",
-                "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202",
-                "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5",
-                "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548",
-                "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a",
-                "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f",
-                "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20",
-                "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218",
-                "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c",
-                "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e",
-                "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56",
-                "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224",
-                "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a",
-                "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2",
-                "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a",
-                "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819",
-                "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346",
-                "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b",
-                "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e",
-                "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534",
-                "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb",
-                "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0",
-                "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156",
-                "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd",
-                "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87",
-                "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc",
-                "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195",
-                "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33",
-                "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f",
-                "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d",
-                "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd",
-                "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728",
-                "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7",
-                "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca",
-                "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99",
-                "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf",
-                "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e",
-                "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c",
-                "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5",
-                "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"
-            ],
-            "version": "==1.14.6"
-        },
-        "cfgv": {
-            "hashes": [
-                "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1",
-                "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"
-            ],
-            "markers": "python_full_version >= '3.6.1'",
-            "version": "==3.3.0"
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
-                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==4.0.0"
-        },
-        "charset-normalizer": {
-            "hashes": [
-                "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
-                "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
-            ],
-            "markers": "python_version >= '3'",
-            "version": "==2.0.4"
-        },
-        "click": {
-            "hashes": [
-                "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
-                "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
-            ],
-            "index": "pypi",
-            "version": "==8.0.1"
-        },
-        "colorama": {
-            "hashes": [
-                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
-                "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==0.4.4"
-        },
-        "coverage": {
-            "hashes": [
-                "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
-                "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
-                "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
-                "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
-                "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
-                "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
-                "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
-                "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
-                "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
-                "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
-                "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
-                "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
-                "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
-                "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
-                "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
-                "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
-                "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
-                "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
-                "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
-                "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
-                "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
-                "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
-                "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
-                "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
-                "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
-                "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
-                "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
-                "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
-                "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
-                "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
-                "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
-                "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
-                "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
-                "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
-                "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
-                "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
-                "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
-                "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
-                "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
-                "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
-                "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
-                "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
-                "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
-                "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
-                "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
-                "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
-                "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
-                "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
-                "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
-                "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
-                "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
-                "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
-            ],
-            "index": "pypi",
-            "version": "==5.5"
-        },
-        "cryptography": {
-            "hashes": [
-                "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
-                "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
-                "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
-                "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
-                "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
-                "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
-                "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
-                "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
-                "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
-                "sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586",
-                "sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3",
-                "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
-                "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
-                "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.4.7"
-        },
-        "dataclasses": {
-            "hashes": [
-                "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf",
-                "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"
-            ],
-            "index": "pypi",
-            "markers": "python_version < '3.7'",
-            "version": "==0.8"
-        },
-        "decorator": {
-            "hashes": [
-                "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323",
-                "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==5.0.9"
-        },
-        "distlib": {
-            "hashes": [
-                "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736",
-                "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"
-            ],
-            "version": "==0.3.2"
-        },
-        "docutils": {
-            "hashes": [
-                "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
-                "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
-            ],
-            "index": "pypi",
-            "version": "==0.17.1"
-        },
-        "execnet": {
-            "hashes": [
-                "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5",
-                "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.9.0"
-        },
-        "filelock": {
-            "hashes": [
-                "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
-                "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
-            ],
-            "version": "==3.0.12"
-        },
-        "flake8": {
-            "hashes": [
-                "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b",
-                "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"
-            ],
-            "index": "pypi",
-            "version": "==3.9.2"
-        },
-        "flake8-bugbear": {
-            "hashes": [
-                "sha256:2346c81f889955b39e4a368eb7d508de723d9de05716c287dc860a4073dc57e7",
-                "sha256:4f305dca96be62bf732a218fe6f1825472a621d3452c5b994d8f89dae21dbafa"
-            ],
-            "index": "pypi",
-            "version": "==21.4.3"
-        },
-        "identify": {
-            "hashes": [
-                "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c",
-                "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"
-            ],
-            "markers": "python_full_version >= '3.6.1'",
-            "version": "==2.2.13"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
-                "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==3.2"
-        },
-        "idna-ssl": {
-            "hashes": [
-                "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
-            ],
-            "markers": "python_version < '3.7'",
-            "version": "==1.1.0"
-        },
-        "imagesize": {
-            "hashes": [
-                "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
-                "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.2.0"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f",
-                "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"
-            ],
-            "markers": "python_version < '3.8'",
-            "version": "==4.6.4"
-        },
-        "importlib-resources": {
-            "hashes": [
-                "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977",
-                "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"
-            ],
-            "markers": "python_version < '3.7'",
-            "version": "==5.2.2"
-        },
-        "iniconfig": {
-            "hashes": [
-                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
-                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
-            ],
-            "version": "==1.1.1"
-        },
-        "ipython": {
-            "hashes": [
-                "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64",
-                "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==7.16.1"
-        },
-        "ipython-genutils": {
-            "hashes": [
-                "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
-                "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
-            ],
-            "version": "==0.2.0"
-        },
-        "jedi": {
-            "hashes": [
-                "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93",
-                "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.18.0"
-        },
-        "jeepney": {
-            "hashes": [
-                "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac",
-                "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"
-            ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==0.7.1"
-        },
-        "jinja2": {
-            "hashes": [
-                "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
-                "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.0.1"
-        },
-        "keyring": {
-            "hashes": [
-                "sha256:b32397fd7e7063f8dd74a26db910c9862fc2109285fa16e3b5208bcb42a3e579",
-                "sha256:b7e0156667f5dcc73c1f63a518005cd18a4eb23fe77321194fefcc03748b21a4"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==23.1.0"
-        },
-        "markdown-it-py": {
-            "hashes": [
-                "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3",
-                "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"
-            ],
-            "markers": "python_version ~= '3.6'",
-            "version": "==1.1.0"
-        },
-        "markupsafe": {
-            "hashes": [
-                "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
-                "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
-                "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
-                "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
-                "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
-                "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
-                "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
-                "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
-                "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
-                "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
-                "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
-                "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
-                "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
-                "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38",
-                "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac",
-                "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
-                "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6",
-                "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
-                "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
-                "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
-                "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
-                "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a",
-                "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
-                "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9",
-                "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864",
-                "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
-                "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
-                "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
-                "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
-                "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
-                "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b",
-                "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
-                "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
-                "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
-                "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
-                "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
-                "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
-                "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
-                "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
-                "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
-                "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
-                "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145",
-                "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
-                "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
-                "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
-                "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
-                "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
-                "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
-                "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
-                "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
-                "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
-                "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
-                "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
-                "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.0.1"
-        },
-        "matplotlib-inline": {
-            "hashes": [
-                "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811",
-                "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==0.1.2"
-        },
-        "mccabe": {
-            "hashes": [
-                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
-                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
-            ],
-            "version": "==0.6.1"
-        },
-        "mdit-py-plugins": {
-            "hashes": [
-                "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c",
-                "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f"
-            ],
-            "markers": "python_version ~= '3.6'",
-            "version": "==0.2.8"
-        },
-        "multidict": {
-            "hashes": [
-                "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a",
-                "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93",
-                "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632",
-                "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656",
-                "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79",
-                "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7",
-                "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d",
-                "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5",
-                "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224",
-                "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26",
-                "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea",
-                "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348",
-                "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6",
-                "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76",
-                "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1",
-                "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f",
-                "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952",
-                "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a",
-                "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37",
-                "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9",
-                "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359",
-                "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8",
-                "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da",
-                "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3",
-                "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d",
-                "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf",
-                "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841",
-                "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d",
-                "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93",
-                "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f",
-                "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647",
-                "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635",
-                "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456",
-                "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda",
-                "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5",
-                "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
-                "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==5.1.0"
-        },
-        "mypy": {
-            "hashes": [
-                "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9",
-                "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a",
-                "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9",
-                "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e",
-                "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2",
-                "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212",
-                "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b",
-                "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885",
-                "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150",
-                "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703",
-                "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072",
-                "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457",
-                "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e",
-                "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0",
-                "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb",
-                "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97",
-                "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8",
-                "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811",
-                "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6",
-                "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de",
-                "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504",
-                "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921",
-                "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"
-            ],
-            "index": "pypi",
-            "version": "==0.910"
-        },
-        "mypy-extensions": {
-            "hashes": [
-                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
-                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
-            ],
-            "index": "pypi",
-            "version": "==0.4.3"
-        },
-        "myst-parser": {
-            "hashes": [
-                "sha256:7c3c78a36c4bc30ce6a67933ebd800a880c8d81f1688fad5c2ebd82cddbc1603",
-                "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade"
-            ],
-            "index": "pypi",
-            "version": "==0.15.1"
-        },
-        "nodeenv": {
-            "hashes": [
-                "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b",
-                "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"
-            ],
-            "version": "==1.6.0"
-        },
-        "packaging": {
-            "hashes": [
-                "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
-                "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==21.0"
-        },
-        "parso": {
-            "hashes": [
-                "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398",
-                "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.8.2"
-        },
-        "pathspec": {
-            "hashes": [
-                "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
-                "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
-            ],
-            "index": "pypi",
-            "version": "==0.9.0"
-        },
-        "pexpect": {
-            "hashes": [
-                "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
-                "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
-            ],
-            "markers": "sys_platform != 'win32'",
-            "version": "==4.8.0"
-        },
-        "pickleshare": {
-            "hashes": [
-                "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
-                "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
-            ],
-            "version": "==0.7.5"
-        },
-        "pkginfo": {
-            "hashes": [
-                "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779",
-                "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"
-            ],
-            "version": "==1.7.1"
-        },
-        "platformdirs": {
-            "hashes": [
-                "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
-                "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
-            ],
-            "index": "pypi",
-            "version": "==2.2.0"
-        },
-        "pluggy": {
-            "hashes": [
-                "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
-                "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.13.1"
-        },
-        "pre-commit": {
-            "hashes": [
-                "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c",
-                "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"
-            ],
-            "index": "pypi",
-            "version": "==2.14.0"
-        },
-        "prompt-toolkit": {
-            "hashes": [
-                "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c",
-                "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"
-            ],
-            "markers": "python_full_version >= '3.6.2'",
-            "version": "==3.0.20"
-        },
-        "ptyprocess": {
-            "hashes": [
-                "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35",
-                "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"
-            ],
-            "version": "==0.7.0"
-        },
-        "py": {
-            "hashes": [
-                "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
-                "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.10.0"
-        },
-        "pycodestyle": {
-            "hashes": [
-                "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
-                "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.7.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:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3",
-                "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.3.1"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
-                "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==2.10.0"
-        },
-        "pyparsing": {
-            "hashes": [
-                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
-                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
-            ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.4.7"
-        },
-        "pytest": {
-            "hashes": [
-                "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
-                "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
-            ],
-            "index": "pypi",
-            "version": "==6.2.4"
-        },
-        "pytest-cov": {
-            "hashes": [
-                "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a",
-                "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"
-            ],
-            "index": "pypi",
-            "version": "==2.12.1"
-        },
-        "pytest-forked": {
-            "hashes": [
-                "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca",
-                "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.3.0"
-        },
-        "pytest-xdist": {
-            "hashes": [
-                "sha256:e8ecde2f85d88fbcadb7d28cb33da0fa29bca5cf7d5967fa89fc0e97e5299ea5",
-                "sha256:ed3d7da961070fce2a01818b51f6888327fb88df4379edeb6b9d990e789d9c8d"
-            ],
-            "index": "pypi",
-            "version": "==2.3.0"
-        },
-        "pytz": {
-            "hashes": [
-                "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
-                "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
-            ],
-            "version": "==2021.1"
-        },
-        "pyyaml": {
-            "hashes": [
-                "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
-                "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
-                "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
-                "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
-                "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
-                "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
-                "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
-                "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
-                "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
-                "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
-                "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
-                "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
-                "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
-                "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
-                "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
-                "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
-                "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
-                "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
-                "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
-                "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
-                "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
-                "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
-                "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
-                "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
-                "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
-                "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
-                "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
-                "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
-                "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==5.4.1"
-        },
-        "readme-renderer": {
-            "hashes": [
-                "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c",
-                "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"
-            ],
-            "index": "pypi",
-            "version": "==29.0"
-        },
-        "regex": {
-            "hashes": [
-                "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd",
-                "sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642",
-                "sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1",
-                "sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321",
-                "sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529",
-                "sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36",
-                "sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a",
-                "sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30",
-                "sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce",
-                "sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376",
-                "sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd",
-                "sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586",
-                "sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7",
-                "sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9",
-                "sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea",
-                "sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94",
-                "sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3",
-                "sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f",
-                "sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267",
-                "sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc",
-                "sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23",
-                "sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882",
-                "sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc",
-                "sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe",
-                "sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759",
-                "sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456",
-                "sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239",
-                "sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb",
-                "sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948",
-                "sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0",
-                "sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183",
-                "sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92",
-                "sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade",
-                "sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044",
-                "sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee",
-                "sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033",
-                "sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2",
-                "sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5",
-                "sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2",
-                "sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504",
-                "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a"
-            ],
-            "index": "pypi",
-            "version": "==2021.8.21"
-        },
-        "requests": {
-            "hashes": [
-                "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
-                "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==2.26.0"
-        },
-        "requests-toolbelt": {
-            "hashes": [
-                "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
-                "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
-            ],
-            "version": "==0.9.1"
-        },
-        "rfc3986": {
-            "hashes": [
-                "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
-                "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
-            ],
-            "version": "==1.5.0"
-        },
-        "secretstorage": {
-            "hashes": [
-                "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f",
-                "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"
-            ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==3.3.1"
-        },
-        "setuptools-scm": {
-            "extras": [
-                "toml"
-            ],
-            "hashes": [
-                "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c",
-                "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==6.0.1"
-        },
-        "six": {
-            "hashes": [
-                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
-                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.16.0"
-        },
-        "snowballstemmer": {
-            "hashes": [
-                "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2",
-                "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"
-            ],
-            "version": "==2.1.0"
-        },
-        "sphinx": {
-            "hashes": [
-                "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13",
-                "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"
-            ],
-            "index": "pypi",
-            "version": "==4.1.2"
-        },
-        "sphinx-copybutton": {
-            "hashes": [
-                "sha256:4340d33c169dac6dd82dce2c83333412aa786a42dd01a81a8decac3b130dc8b0",
-                "sha256:8daed13a87afd5013c3a9af3575cc4d5bec052075ccd3db243f895c07a689386"
-            ],
-            "index": "pypi",
-            "version": "==0.4.0"
-        },
-        "sphinxcontrib-applehelp": {
-            "hashes": [
-                "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
-                "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.2"
-        },
-        "sphinxcontrib-devhelp": {
-            "hashes": [
-                "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
-                "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.2"
-        },
-        "sphinxcontrib-htmlhelp": {
-            "hashes": [
-                "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07",
-                "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.0.0"
-        },
-        "sphinxcontrib-jsmath": {
-            "hashes": [
-                "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
-                "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.1"
-        },
-        "sphinxcontrib-programoutput": {
-            "hashes": [
-                "sha256:0ef1c1d9159dbe7103077748214305eb4e0138e861feb71c0c346afc5fe97f84",
-                "sha256:300ee9b8caee8355d25cc74b4d1c7efd12e608d2ad165e3141d31e6fbc152b7f"
-            ],
-            "index": "pypi",
-            "version": "==0.17"
-        },
-        "sphinxcontrib-qthelp": {
-            "hashes": [
-                "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
-                "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.3"
-        },
-        "sphinxcontrib-serializinghtml": {
-            "hashes": [
-                "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd",
-                "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.1.5"
-        },
-        "tokenize-rt": {
-            "hashes": [
-                "sha256:ab339b5ff829eb5e198590477f9c03c84e762b3e455e74c018956e7e326cbc70",
-                "sha256:b37251fa28c21e8cce2e42f7769a35fba2dd2ecafb297208f9a9a8add3ca7793"
-            ],
-            "markers": "python_full_version >= '3.6.1'",
-            "version": "==4.1.0"
-        },
-        "toml": {
-            "hashes": [
-                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
-                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
-            ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.10.2"
-        },
-        "tomli": {
-            "hashes": [
-                "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f",
-                "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"
-            ],
-            "index": "pypi",
-            "version": "==1.2.1"
-        },
-        "tox": {
-            "hashes": [
-                "sha256:9fbf8e2ab758b2a5e7cb2c72945e4728089934853076f67ef18d7575c8ab6b88",
-                "sha256:c6c4e77705ada004283610fd6d9ba4f77bc85d235447f875df9f0ba1bc23b634"
-            ],
-            "index": "pypi",
-            "version": "==3.24.3"
-        },
-        "tqdm": {
-            "hashes": [
-                "sha256:07856e19a1fe4d2d9621b539d3f072fa88c9c1ef1f3b7dd4d4953383134c3164",
-                "sha256:35540feeaca9ac40c304e916729e6b78045cbbeccd3e941b2868f09306798ac9"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==4.62.1"
-        },
-        "traitlets": {
-            "hashes": [
-                "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
-                "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
-            ],
-            "version": "==4.3.3"
-        },
-        "twine": {
-            "hashes": [
-                "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218",
-                "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"
-            ],
-            "index": "pypi",
-            "version": "==3.4.2"
-        },
-        "typed-ast": {
-            "hashes": [
-                "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1",
-                "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d",
-                "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6",
-                "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd",
-                "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37",
-                "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151",
-                "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07",
-                "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440",
-                "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70",
-                "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496",
-                "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea",
-                "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400",
-                "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc",
-                "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606",
-                "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc",
-                "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581",
-                "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412",
-                "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a",
-                "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2",
-                "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787",
-                "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f",
-                "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937",
-                "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64",
-                "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487",
-                "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b",
-                "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41",
-                "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a",
-                "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3",
-                "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166",
-                "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"
-            ],
-            "index": "pypi",
-            "version": "==1.4.3"
-        },
-        "types-dataclasses": {
-            "hashes": [
-                "sha256:248075d093d8f7c1541ce515594df7ae40233d1340afde11ce7125368c5209b8",
-                "sha256:fc372bb68b878ac7a68fd04230d923d4a6303a137ecb0b9700b90630bdfcbfc9"
-            ],
-            "index": "pypi",
-            "version": "==0.1.7"
-        },
-        "types-pyyaml": {
-            "hashes": [
-                "sha256:745dcb4b1522423026bcc83abb9925fba747f1e8602d902f71a4058f9e7fb662",
-                "sha256:96f8d3d96aa1a18a465e8f6a220e02cff2f52632314845a364ecbacb0aea6e30"
-            ],
-            "index": "pypi",
-            "version": "==5.4.6"
-        },
-        "types-typed-ast": {
-            "hashes": [
-                "sha256:b7f561796b4d002c7522b0020f58b18f715bd28a31429d424a78e2e2dbbd6785",
-                "sha256:ffa0471e0ba19c4ea0cba0436d660871b5f5215854ea9ead3cb5b60f525af75a"
-            ],
-            "index": "pypi",
-            "version": "==1.4.4"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
-                "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
-                "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
-            ],
-            "index": "pypi",
-            "markers": "python_version < '3.10'",
-            "version": "==3.10.0.0"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
-                "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
-            ],
-            "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.26.6"
-        },
-        "virtualenv": {
-            "hashes": [
-                "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0",
-                "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==20.7.2"
-        },
-        "wcwidth": {
-            "hashes": [
-                "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
-                "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
-            ],
-            "version": "==0.2.5"
-        },
-        "webencodings": {
-            "hashes": [
-                "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
-                "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
-            ],
-            "version": "==0.5.1"
-        },
-        "wheel": {
-            "hashes": [
-                "sha256:21014b2bd93c6d0034b6ba5d35e4eb284340e09d63c59aef6fc14b0f346146fd",
-                "sha256:e2ef7239991699e3355d54f8e968a21bb940a1dbf34a4d226741e64462516fad"
-            ],
-            "index": "pypi",
-            "version": "==0.37.0"
-        },
-        "yarl": {
-            "hashes": [
-                "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e",
-                "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434",
-                "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366",
-                "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3",
-                "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec",
-                "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959",
-                "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e",
-                "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c",
-                "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6",
-                "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a",
-                "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6",
-                "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424",
-                "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e",
-                "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f",
-                "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50",
-                "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2",
-                "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc",
-                "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4",
-                "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970",
-                "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10",
-                "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0",
-                "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406",
-                "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896",
-                "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643",
-                "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721",
-                "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478",
-                "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724",
-                "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e",
-                "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8",
-                "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96",
-                "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25",
-                "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76",
-                "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2",
-                "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2",
-                "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c",
-                "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
-                "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.6.3"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
-                "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.5.0"
-        }
-    }
-}
index 7bf0ed8d16f59d767c2a2e41d45700d4f1c6d409..cad8184f7bc36ad02a010393e1bf59d430b65f1e 100644 (file)
@@ -1,15 +1,14 @@
-![Black Logo](https://raw.githubusercontent.com/psf/black/main/docs/_static/logo2-readme.png)
+[![Black Logo](https://raw.githubusercontent.com/psf/black/main/docs/_static/logo2-readme.png)](https://black.readthedocs.io/en/stable/)
 
 <h2 align="center">The Uncompromising Code Formatter</h2>
 
 <p align="center">
 <a href="https://github.com/psf/black/actions"><img alt="Actions Status" src="https://github.com/psf/black/workflows/Test/badge.svg"></a>
-<a href="https://github.com/psf/black/actions"><img alt="Actions Status" src="https://github.com/psf/black/workflows/Primer/badge.svg"></a>
 <a href="https://black.readthedocs.io/en/stable/?badge=stable"><img alt="Documentation Status" src="https://readthedocs.org/projects/black/badge/?version=stable"></a>
 <a href="https://coveralls.io/github/psf/black?branch=main"><img alt="Coverage Status" src="https://coveralls.io/repos/github/psf/black/badge.svg?branch=main"></a>
 <a href="https://github.com/psf/black/blob/main/LICENSE"><img alt="License: MIT" src="https://black.readthedocs.io/en/stable/_static/license.svg"></a>
 <a href="https://pypi.org/project/black/"><img alt="PyPI" src="https://img.shields.io/pypi/v/black"></a>
-<a href="https://pepy.tech/project/black"><img alt="Downloads" src="https://pepy.tech/badge/black"></a>
+<a href="https://pepy.tech/project/black"><img alt="Downloads" src="https://static.pepy.tech/badge/black"></a>
 <a href="https://anaconda.org/conda-forge/black/"><img alt="conda-forge" src="https://img.shields.io/conda/dn/conda-forge/black.svg?label=conda-forge"></a>
 <a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
 </p>
@@ -39,14 +38,12 @@ Try it out now using the [Black Playground](https://black.vercel.app). Watch the
 
 ### Installation
 
-_Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to
-run. If you want to format Python 2 code as well, install with
-`pip install black[python2]`. If you want to format Jupyter Notebooks, install with
-`pip install black[jupyter]`.
+_Black_ can be installed by running `pip install black`. It requires Python 3.8+ to run.
+If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`.
 
 If you can't wait for the latest _hotness_ and want to install from GitHub, use:
 
-`pip install git+git://github.com/psf/black`
+`pip install git+https://github.com/psf/black`
 
 ### Usage
 
@@ -66,16 +63,13 @@ Further information can be found in our docs:
 
 - [Usage and Configuration](https://black.readthedocs.io/en/stable/usage_and_configuration/index.html)
 
-### NOTE: This is a beta product
-
 _Black_ is already [successfully used](https://github.com/psf/black#used-by) by many
-projects, small and big. Black has a comprehensive test suite, with efficient parallel
-tests, and our own auto formatting and parallel Continuous Integration runner. However,
-_Black_ is still beta. 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.
+projects, small and big. _Black_ has a comprehensive test suite, with efficient parallel
+tests, and our own auto formatting and parallel Continuous Integration runner. Now that
+we have become stable, you should not expect large formatting changes in the future.
+Stylistic changes will mostly be responses to bug reports and support for new Python
+syntax. For more information please refer to the
+[The Black Code Style](https://black.readthedocs.io/en/stable/the_black_code_style/index.html).
 
 Also, as a safety measure which slows down processing, _Black_ will check that the
 reformatted code still produces a valid AST that is effectively equivalent to the
@@ -87,7 +81,9 @@ section for details). If you're feeling confident, use `--fast`.
 
 _Black_ is a PEP 8 compliant opinionated formatter. _Black_ reformats entire files in
 place. Style configuration options are deliberately limited and rarely added. It doesn't
-take previous formatting into account (see [Pragmatism](#pragmatism) for exceptions).
+take previous formatting into account (see
+[Pragmatism](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#pragmatism)
+for exceptions).
 
 Our documentation covers the current _Black_ code style, but planned changes to it are
 also documented. They're both worth taking a look:
@@ -95,6 +91,10 @@ also documented. They're both worth taking a look:
 - [The _Black_ Code Style: Current style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html)
 - [The _Black_ Code Style: Future style](https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html)
 
+Changes to the _Black_ code style are bound by the Stability Policy:
+
+- [The _Black_ Code Style: Stability Policy](https://black.readthedocs.io/en/stable/the_black_code_style/index.html#stability-policy)
+
 Please refer to this document before submitting an issue. What seems like a bug might be
 intended behaviour.
 
@@ -132,13 +132,13 @@ code in compliance with many other _Black_ formatted projects.
 ## Used by
 
 The following notable open-source projects trust _Black_ with enforcing a consistent
-code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy,
-Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow,
-Twisted, LocalStack, every Datadog Agent Integration, Home Assistant, Zulip, Kedro, and
-many more.
+code style: pytest, tox, Pyramid, Django, Django Channels, Hypothesis, attrs,
+SQLAlchemy, Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv),
+pandas, Pillow, Twisted, LocalStack, every Datadog Agent Integration, Home Assistant,
+Zulip, Kedro, OpenOA, FLORIS, ORBIT, WOMBAT, and many more.
 
-The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora, Duolingo,
-QuantumBlack.
+The following organizations use _Black_: Facebook, Dropbox, KeepTruckin, Lyft, Mozilla,
+Quora, Duolingo, QuantumBlack, Tesla, Archer Aviation.
 
 Are we missing anyone? Let us know.
 
@@ -165,8 +165,8 @@ Twisted and CPython:
 
 > At least the name is good.
 
-**Kenneth Reitz**, creator of [`requests`](http://python-requests.org/) and
-[`pipenv`](https://readthedocs.org/projects/pipenv/):
+**Kenneth Reitz**, creator of [`requests`](https://requests.readthedocs.io/en/latest/)
+and [`pipenv`](https://readthedocs.org/projects/pipenv/):
 
 > This vastly improves the formatting of our code. Thanks a ton!
 
diff --git a/.vim/bundle/black/SECURITY.md b/.vim/bundle/black/SECURITY.md
new file mode 100644 (file)
index 0000000..4704950
--- /dev/null
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Supported Versions
+
+Only the latest non-prerelease version is supported.
+
+## Security contact information
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the
+fix and disclosure.
index ddf07933a3e2dd119d40bf4b75a848712db7cb7c..8b698ae3c8012b278e3818a2d0e0290d69eb171b 100644 (file)
@@ -5,13 +5,18 @@ inputs:
   options:
     description:
       "Options passed to Black. Use `black --help` to see available options. Default:
-      '--check'"
+      '--check --diff'"
     required: false
     default: "--check --diff"
   src:
     description: "Source to run Black. Default: '.'"
     required: false
     default: "."
+  jupyter:
+    description:
+      "Set this option to true to include Jupyter Notebook files. Default: false"
+    required: false
+    default: false
   black_args:
     description: "[DEPRECATED] Black input arguments."
     required: false
@@ -28,31 +33,32 @@ branding:
 runs:
   using: composite
   steps:
-    - run: |
-        # Exists since using github.action_path + path to main script doesn't work because bash
-        # interprets the backslashes in github.action_path (which are used when the runner OS
-        # is Windows) destroying the path to the target file.
-        #
-        # Also semicolons are necessary because I can't get the newlines to work
-        entrypoint="import sys;
-        import subprocess;
-        from pathlib import Path;
-
-        MAIN_SCRIPT = Path(r'${{ github.action_path }}') / 'action' / 'main.py';
-
-        proc = subprocess.run([sys.executable, str(MAIN_SCRIPT)]);
-        sys.exit(proc.returncode)
-        "
-
+    - name: black
+      run: |
         if [ "$RUNNER_OS" == "Windows" ]; then
-          echo $entrypoint | python
+          runner="python"
         else
-          echo $entrypoint | python3
+          runner="python3"
         fi
+
+        out=$(${runner} $GITHUB_ACTION_PATH/action/main.py)
+        exit_code=$?
+
+        # Display the raw output in the step
+        echo "${out}"
+
+        # Display the Markdown output in the job summary
+        echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY
+        echo "${out}" >> $GITHUB_STEP_SUMMARY
+        echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
+
+        # Exit with the exit-code returned by Black
+        exit ${exit_code}
       env:
         # TODO: Remove once https://github.com/actions/runner/issues/665 is fixed.
         INPUT_OPTIONS: ${{ inputs.options }}
         INPUT_SRC: ${{ inputs.src }}
+        INPUT_JUPYTER: ${{ inputs.jupyter }}
         INPUT_BLACK_ARGS: ${{ inputs.black_args }}
         INPUT_VERSION: ${{ inputs.version }}
         pythonioencoding: utf-8
index fde312553bf3b8554675c42009011af94b55b6fd..c0af3930dbbe6f31745e941712e81df3ddb2cae3 100644 (file)
@@ -1,39 +1,79 @@
 import os
 import shlex
+import shutil
 import sys
 from pathlib import Path
-from subprocess import run, PIPE, STDOUT
+from subprocess import PIPE, STDOUT, run
 
 ACTION_PATH = Path(os.environ["GITHUB_ACTION_PATH"])
 ENV_PATH = ACTION_PATH / ".black-env"
 ENV_BIN = ENV_PATH / ("Scripts" if sys.platform == "win32" else "bin")
 OPTIONS = os.getenv("INPUT_OPTIONS", default="")
 SRC = os.getenv("INPUT_SRC", default="")
+JUPYTER = os.getenv("INPUT_JUPYTER") == "true"
 BLACK_ARGS = os.getenv("INPUT_BLACK_ARGS", default="")
 VERSION = os.getenv("INPUT_VERSION", default="")
 
 run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True)
 
-req = "black[colorama,python2]"
-if VERSION:
-    req += f"=={VERSION}"
+version_specifier = VERSION
+if VERSION and VERSION[0] in "0123456789":
+    version_specifier = f"=={VERSION}"
+if JUPYTER:
+    extra_deps = "[colorama,jupyter]"
+else:
+    extra_deps = "[colorama]"
+if version_specifier:
+    req = f"black{extra_deps}{version_specifier}"
+else:
+    describe_name = ""
+    with open(ACTION_PATH / ".git_archival.txt", encoding="utf-8") as fp:
+        for line in fp:
+            if line.startswith("describe-name: "):
+                describe_name = line[len("describe-name: ") :].rstrip()
+                break
+    if not describe_name:
+        print("::error::Failed to detect action version.", file=sys.stderr, flush=True)
+        sys.exit(1)
+    # expected format is one of:
+    # - 23.1.0
+    # - 23.1.0-51-g448bba7
+    if describe_name.count("-") < 2:
+        # the action's commit matches a tag exactly, install exact version from PyPI
+        req = f"black{extra_deps}=={describe_name}"
+    else:
+        # the action's commit does not match any tag, install from the local git repo
+        req = f".{extra_deps}"
+print(f"Installing {req}...", flush=True)
 pip_proc = run(
     [str(ENV_BIN / "python"), "-m", "pip", "install", req],
     stdout=PIPE,
     stderr=STDOUT,
     encoding="utf-8",
+    cwd=ACTION_PATH,
 )
 if pip_proc.returncode:
     print(pip_proc.stdout)
-    print("::error::Failed to install Black.", flush=True)
+    print("::error::Failed to install Black.", file=sys.stderr, flush=True)
     sys.exit(pip_proc.returncode)
 
 
 base_cmd = [str(ENV_BIN / "black")]
 if BLACK_ARGS:
     # TODO: remove after a while since this is deprecated in favour of SRC + OPTIONS.
-    proc = run([*base_cmd, *shlex.split(BLACK_ARGS)])
+    proc = run(
+        [*base_cmd, *shlex.split(BLACK_ARGS)],
+        stdout=PIPE,
+        stderr=STDOUT,
+        encoding="utf-8",
+    )
 else:
-    proc = run([*base_cmd, *shlex.split(OPTIONS), *shlex.split(SRC)])
-
+    proc = run(
+        [*base_cmd, *shlex.split(OPTIONS), *shlex.split(SRC)],
+        stdout=PIPE,
+        stderr=STDOUT,
+        encoding="utf-8",
+    )
+shutil.rmtree(ENV_PATH, ignore_errors=True)
+print(proc.stdout)
 sys.exit(proc.returncode)
index 9ff5c2341fe71d7920aa8a35fbbdcb9496a40e66..051fea05c3bd1d87d98cd53f59cebf78031ebcdb 100644 (file)
@@ -3,8 +3,13 @@ import collections
 import os
 import sys
 import vim
-from distutils.util import strtobool
 
+def strtobool(text):
+  if text.lower() in ['y', 'yes', 't', 'true', 'on', '1']:
+    return True
+  if text.lower() in ['n', 'no', 'f', 'false', 'off', '0']:
+    return False
+  raise ValueError(f"{text} is not convertible to boolean")
 
 class Flag(collections.namedtuple("FlagBase", "name, cast")):
   @property
@@ -24,10 +29,12 @@ FLAGS = [
   Flag(name="fast", cast=strtobool),
   Flag(name="skip_string_normalization", cast=strtobool),
   Flag(name="quiet", cast=strtobool),
+  Flag(name="skip_magic_trailing_comma", cast=strtobool),
+  Flag(name="preview", cast=strtobool),
 ]
 
 
-def _get_python_binary(exec_prefix):
+def _get_python_binary(exec_prefix, pyver):
   try:
     default = vim.eval("g:pymode_python").strip()
   except vim.error:
@@ -36,7 +43,15 @@ def _get_python_binary(exec_prefix):
     return default
   if sys.platform[:3] == "win":
     return exec_prefix / 'python.exe'
-  return exec_prefix / 'bin' / 'python3'
+  bin_path = exec_prefix / "bin"
+  exec_path = (bin_path / f"python{pyver[0]}.{pyver[1]}").resolve()
+  if exec_path.exists():
+    return exec_path
+  # It is possible that some environments may only have python3
+  exec_path = (bin_path / f"python3").resolve()
+  if exec_path.exists():
+    return exec_path
+  raise ValueError("python executable not found")
 
 def _get_pip(venv_path):
   if sys.platform[:3] == "win":
@@ -49,9 +64,19 @@ def _get_virtualenv_site_packages(venv_path, pyver):
   return venv_path / 'lib' / f'python{pyver[0]}.{pyver[1]}' / 'site-packages'
 
 def _initialize_black_env(upgrade=False):
+  if vim.eval("g:black_use_virtualenv ? 'true' : 'false'") == "false":
+    if upgrade:
+      print("Upgrade disabled due to g:black_use_virtualenv being disabled.")
+      print("Either use your system package manager (or pip) to upgrade black separately,")
+      print("or modify your vimrc to have 'let g:black_use_virtualenv = 1'.")
+      return False
+    else:
+      # Nothing needed to be done.
+      return True
+
   pyver = sys.version_info[:3]
-  if pyver < (3, 6, 2):
-    print("Sorry, Black requires Python 3.6.2+ to run.")
+  if pyver < (3, 8):
+    print("Sorry, Black requires Python 3.8+ to run.")
     return False
 
   from pathlib import Path
@@ -65,7 +90,7 @@ def _initialize_black_env(upgrade=False):
     _executable = sys.executable
     _base_executable = getattr(sys, "_base_executable", _executable)
     try:
-      executable = str(_get_python_binary(Path(sys.exec_prefix)))
+      executable = str(_get_python_binary(Path(sys.exec_prefix), pyver))
       sys.executable = executable
       sys._base_executable = executable
       print(f'Creating a virtualenv in {virtualenv_path}...')
@@ -138,6 +163,8 @@ def Black(**kwargs):
     line_length=configs["line_length"],
     string_normalization=not configs["skip_string_normalization"],
     is_pyi=vim.current.buffer.name.endswith('.pyi'),
+    magic_trailing_comma=not configs["skip_magic_trailing_comma"],
+    preview=configs["preview"],
     **black_kwargs,
   )
   quiet = configs["quiet"]
@@ -151,9 +178,9 @@ def Black(**kwargs):
     )
   except black.NothingChanged:
     if not quiet:
-      print(f'Already well formatted, good job. (took {time.time() - start:.4f}s)')
+      print(f'Black: already well formatted, good job. (took {time.time() - start:.4f}s)')
   except Exception as exc:
-    print(exc)
+    print(f'Black: {exc}')
   else:
     current_buffer = vim.current.window.buffer
     cursors = []
@@ -170,7 +197,7 @@ def Black(**kwargs):
       except vim.error:
         window.cursor = (len(window.buffer), 0)
     if not quiet:
-      print(f'Reformatted in {time.time() - start:.4f}s.')
+      print(f'Black: reformatted in {time.time() - start:.4f}s.')
 
 def get_configs():
   filename = vim.eval("@%")
diff --git a/.vim/bundle/black/docs/_static/custom.css b/.vim/bundle/black/docs/_static/custom.css
deleted file mode 100644 (file)
index eacd69c..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Make the sidebar scrollable. Fixes https://github.com/psf/black/issues/990 */
-div.sphinxsidebar {
-  max-height: calc(100% - 18px);
-  overflow-y: auto;
-}
-
-/* Hide scrollbar for Chrome, Safari and Opera */
-div.sphinxsidebar::-webkit-scrollbar {
-  display: none;
-}
-
-/* Hide scrollbar for IE 6, 7 and 8 */
-@media \0screen\, screen\9 {
-  div.sphinxsidebar {
-    -ms-overflow-style: none;
-  }
-}
-
-/* Hide scrollbar for IE 9 and 10 */
-/* backslash-9 removes ie11+ & old Safari 4 */
-@media screen and (min-width: 0\0) {
-  div.sphinxsidebar {
-    -ms-overflow-style: none\9;
-  }
-}
-
-/* Hide scrollbar for IE 11 and up */
-_:-ms-fullscreen,
-:root div.sphinxsidebar {
-  -ms-overflow-style: none;
-}
-
-/* Hide scrollbar for Edge */
-@supports (-ms-ime-align: auto) {
-  div.sphinxsidebar {
-    -ms-overflow-style: none;
-  }
-}
-
-/* Nicer style for local document toc */
-.contents.topic {
-  background: none;
-  border: none;
-}
index 7abddd2c330c4720efaa8ee910342d8d864538fa..e863488dfbc8b605882da675729d0089fb6a2594 100644 (file)
@@ -1,5 +1,2 @@
-[MESSAGES CONTROL]
-disable = C0330, C0326
-
 [format]
 max-line-length = 88
index 49ad7a2c7716f9329fe21509cab2a3da6a8109d3..ef51f98a966912189c428d1cfebc5067b079c768 100644 (file)
@@ -1,5 +1,2 @@
-[tool.pylint.messages_control]
-disable = "C0330, C0326"
-
 [tool.pylint.format]
 max-line-length = "88"
index 3ada24530ea5ac462faff3f8c21a37af3d2796b7..0b754cdc0f0a2b1a2991c14c52d9595c4b7fc6a0 100644 (file)
@@ -1,5 +1,2 @@
 [pylint]
 max-line-length = 88
-
-[pylint.messages_control]
-disable = C0330, C0326
index 55d0fa99dc6c2ce290fe0c7ec9b1b31ae7590bf5..6b6454353258fa2f051f872fec8efcaef173facf 100644 (file)
@@ -55,7 +55,7 @@ make_pypi_svg(release)
 # -- General configuration ---------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-needs_sphinx = "3.0"
+needs_sphinx = "4.4"
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@@ -87,7 +87,7 @@ master_doc = "index"
 #
 # This is also used if you do content translation via gettext catalogs.
 # Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
@@ -105,39 +105,22 @@ myst_heading_anchors = 3
 # Prettier support formatting some MyST syntax but not all, so let's disable the
 # unsupported yet still enabled by default ones.
 myst_disable_syntax = [
+    "colon_fence",
     "myst_block_break",
     "myst_line_comment",
     "math_block",
 ]
 
+# Optional MyST Syntaxes
+myst_enable_extensions = []
+
 # -- Options for HTML output -------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = "alabaster"
-
-html_sidebars = {
-    "**": [
-        "about.html",
-        "navigation.html",
-        "relations.html",
-        "searchbox.html",
-    ]
-}
-
-html_theme_options = {
-    "show_related": False,
-    "description": "“Any color you like.”",
-    "github_button": True,
-    "github_user": "psf",
-    "github_repo": "black",
-    "github_type": "star",
-    "show_powered_by": True,
-    "fixed_sidebar": True,
-    "logo": "logo2.png",
-}
-
+html_theme = "furo"
+html_logo = "_static/logo2-readme.png"
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -227,7 +210,14 @@ epub_exclude_files = ["search.html"]
 
 autodoc_member_order = "bysource"
 
+#  -- sphinx-copybutton configuration ----------------------------------------
+copybutton_prompt_text = (
+    r">>> |\.\.\. |> |\$ |\# | In \[\d*\]: | {2,5}\.\.\.: | {5,8}: "
+)
+copybutton_prompt_is_regexp = True
+copybutton_remove_prompts = True
+
 # -- Options for intersphinx extension ---------------------------------------
 
 # Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {"https://docs.python.org/3/": None}
+intersphinx_mapping = {"<name>": ("https://docs.python.org/3/", None)}
index b41c7a35dda06501c448e3d99db963438e1eb24a..8562a83ed0c8da7d5e33d3aced1d26bdab02ca23 100644 (file)
@@ -7,36 +7,52 @@ It's recommended you evaluate the quantifiable changes your _Black_ formatting
 modification causes before submitting a PR. Think about if the change seems disruptive
 enough to cause frustration to projects that are already "black formatted".
 
-## black-primer
+## diff-shades
 
-`black-primer` is a tool built for CI (and humans) to have _Black_ `--check` a number of
-Git accessible projects in parallel. (configured in `primer.json`) _(A PR will be
-accepted to add Mercurial support.)_
+diff-shades is a tool that runs _Black_ across a list of open-source projects recording
+the results. The main highlight feature of diff-shades is being able to compare two
+revisions of _Black_. This is incredibly useful as it allows us to see what exact
+changes will occur, say merging a certain PR.
 
-### Run flow
+For more information, please see the [diff-shades documentation][diff-shades].
 
-- Ensure we have a `black` + `git` in PATH
-- Load projects from `primer.json`
-- Run projects in parallel with `--worker` workers (defaults to CPU count / 2)
-  - Checkout projects
-  - Run black and record result
-  - Clean up repository checkout _(can optionally be disabled via `--keep`)_
-- Display results summary to screen
-- Default to cleaning up `--work-dir` (which defaults to tempfile schemantics)
-- Return
-  - 0 for successful run
-  - \< 0 for environment / internal error
-  - \> 0 for each project with an error
+### CI integration
 
-### Speed up runs 🏎
+diff-shades is also the tool behind the "diff-shades results comparing ..." /
+"diff-shades reports zero changes ..." comments on PRs. The project has a GitHub Actions
+workflow that analyzes and compares two revisions of _Black_ according to these rules:
 
-If you're running locally yourself to test black on lots of code try:
+|                       | Baseline revision       | Target revision              |
+| --------------------- | ----------------------- | ---------------------------- |
+| On PRs                | latest commit on `main` | PR commit with `main` merged |
+| On pushes (main only) | latest PyPI version     | the pushed commit            |
 
-- Using `-k` / `--keep` + `-w` / `--work-dir` so you don't have to re-checkout the repo
-  each run
+For pushes to main, there's only one analysis job named `preview-changes` where the
+preview style is used for all projects.
 
-### CLI arguments
+For PRs they get one more analysis job: `assert-no-changes`. It's similar to
+`preview-changes` but runs with the stable code style. It will fail if changes were
+made. This makes sure code won't be reformatted again and again within the same year in
+accordance to Black's stability policy.
 
-```{program-output} black-primer --help
+Additionally for PRs, a PR comment will be posted embedding a summary of the preview
+changes and links to further information. If there's a pre-existing diff-shades comment,
+it'll be updated instead the next time the workflow is triggered on the same PR.
 
+```{note}
+The `preview-changes` job will only fail intentionally if while analyzing a file failed to
+format. Otherwise a failure indicates a bug in the workflow.
 ```
+
+The workflow uploads several artifacts upon completion:
+
+- The raw analyses (.json)
+- HTML diffs (.html)
+- `.pr-comment.json` (if triggered by a PR)
+
+The last one is downloaded by the `diff-shades-comment` workflow and shouldn't be
+downloaded locally. The HTML diffs come in handy for push-based where there's no PR to
+post a comment. And the analyses exist just in case you want to do further analysis
+using the collected data locally.
+
+[diff-shades]: https://github.com/ichard26/diff-shades#readme
diff --git a/.vim/bundle/black/docs/contributing/index.md b/.vim/bundle/black/docs/contributing/index.md
new file mode 100644 (file)
index 0000000..3314c8e
--- /dev/null
@@ -0,0 +1,50 @@
+# Contributing
+
+```{toctree}
+---
+hidden:
+---
+
+the_basics
+gauging_changes
+issue_triage
+release_process
+reference/reference_summary
+```
+
+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?
+
+```{rubric} Bird's eye view
+
+```
+
+In terms of inspiration, _Black_ is about as configurable as _gofmt_ (which is to say,
+not very). This is deliberate. _Black_ aims to provide a consistent style and take away
+opportunities for arguing about style.
+
+Bug reports and fixes are always welcome! Please follow the
+[issue templates on GitHub](https://github.com/psf/black/issues/new/choose) 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.
+
+```{rubric} Contents
+
+```
+
+This section covers the following topics:
+
+- {doc}`the_basics`
+- {doc}`gauging_changes`
+- {doc}`release_process`
+- {doc}`reference/reference_summary`
+
+For an overview on contributing to the _Black_, please checkout {doc}`the_basics`.
+
+If you need a reference of the functions, classes, etc. available to you while
+developing _Black_, there's the {doc}`reference/reference_summary` docs.
diff --git a/.vim/bundle/black/docs/contributing/index.rst b/.vim/bundle/black/docs/contributing/index.rst
deleted file mode 100644 (file)
index 480dbd6..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-Contributing
-============
-
-.. toctree::
-  :hidden:
-
-  the_basics
-  gauging_changes
-  issue_triage
-  release_process
-  reference/reference_summary
-
-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?
-
-.. rubric:: Bird's eye view
-
-In terms of inspiration, *Black* is about as configurable as *gofmt* (which is to say,
-not very). This is deliberate. *Black* aims to provide a consistent style and take away
-opportunities for arguing about style.
-
-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.
-
-.. rubric:: Contents
-
-This section covers the following topics:
-
-- :doc:`the_basics`
-- :doc:`gauging_changes`
-- :doc:`release_process`
-- :doc:`reference/reference_summary`
-
-For an overview on contributing to the *Black*, please checkout :doc:`the_basics`.
-
-If you need a reference of the functions, classes, etc. available to you while
-developing *Black*, there's the :doc:`reference/reference_summary` docs.
index 9b987fb24250cad9bdcfbb9509c198bf8c76a5cf..89cfff76f7fd1bc20e9a05c4d672cd9891e532eb 100644 (file)
@@ -1,6 +1,6 @@
 # Issue triage
 
-Currently, _Black_ uses the issue tracker for bugs, feature requests, proposed design
+Currently, _Black_ uses the issue tracker for bugs, feature requests, proposed style
 modifications, and general user support. Each of these issues have to be triaged so they
 can be eventually be resolved somehow. This document outlines the triaging process and
 also the current guidelines and recommendations.
@@ -42,7 +42,7 @@ The lifecycle of a bug report or user support issue typically goes something lik
 1. _the issue is waiting for triage_
 2. **identified** - has been marked with a type label and other relevant labels, more
    details or a functional reproduction may be still needed (and therefore should be
-   marked with `S: needs repro` or `S: awaiting reponse`)
+   marked with `S: needs repro` or `S: awaiting response`)
 3. **confirmed** - the issue can reproduced and necessary details have been provided
 4. **discussion** - initial triage has been done and now the general details on how the
    issue should be best resolved are being hashed out
@@ -53,13 +53,13 @@ The lifecycle of a bug report or user support issue typically goes something lik
    - the issue has been fixed
    - duplicate of another pre-existing issue or is invalid
 
-For enhancement, documentation, and design issues, the lifecycle looks very similar but
+For enhancement, documentation, and style issues, the lifecycle looks very similar but
 the details are different:
 
 1. _the issue is waiting for triage_
 2. **identified** - has been marked with a type label and other relevant labels
 3. **discussion** - the merits of the suggested changes are currently being discussed, a
-   PR would be acceptable but would be at sigificant risk of being rejected
+   PR would be acceptable but would be at significant risk of being rejected
 4. **accepted & awaiting PR** - it's been determined the suggested changes are OK and a
    PR would be welcomed (`S: accepted`)
 5. **closed**: - the issue has been resolved, reasons include:
index fa765961e690d19312a7dfa12e50cb0c1d17d671..dc615579e307aefedadfff6260389cdd6a27c290 100644 (file)
@@ -3,6 +3,9 @@
 
 *Contents are subject to change.*
 
+Black Classes
+~~~~~~~~~~~~~~
+
 .. currentmodule:: black
 
 :class:`BracketTracker`
 .. autoclass:: black.brackets.BracketTracker
     :members:
 
-:class:`EmptyLineTracker`
+:class:`Line`
+-------------
+
+.. autoclass:: black.lines.Line
+    :members:
+    :special-members: __str__, __bool__
+
+:class:`RHSResult`
 -------------------------
 
-.. autoclass:: black.EmptyLineTracker
+.. autoclass:: black.lines.RHSResult
     :members:
 
-:class:`Line`
--------------
+:class:`LinesBlock`
+-------------------------
 
-.. autoclass:: black.Line
+.. autoclass:: black.lines.LinesBlock
+    :members:
+
+:class:`EmptyLineTracker`
+-------------------------
+
+.. autoclass:: black.lines.EmptyLineTracker
     :members:
-    :special-members: __str__, __bool__
 
 :class:`LineGenerator`
 ----------------------
 
-.. autoclass:: black.LineGenerator
+.. autoclass:: black.linegen.LineGenerator
     :show-inheritance:
     :members:
 
 .. autoclass:: black.comments.ProtoComment
     :members:
 
+:class:`Mode`
+---------------------
+
+.. autoclass:: black.mode.Mode
+    :members:
+
 :class:`Report`
 ---------------
 
-.. autoclass:: black.Report
+.. autoclass:: black.report.Report
     :members:
     :special-members: __str__
 
+:class:`Ok`
+---------------
+
+.. autoclass:: black.rusty.Ok
+    :show-inheritance:
+    :members:
+
+:class:`Err`
+---------------
+
+.. autoclass:: black.rusty.Err
+    :show-inheritance:
+    :members:
+
 :class:`Visitor`
 ----------------
 
     :show-inheritance:
     :members:
 
-Enums
-=====
+:class:`StringTransformer`
+----------------------------
 
-:class:`Changed`
-----------------
+.. autoclass:: black.trans.StringTransformer
+    :show-inheritance:
+    :members:
+
+:class:`CustomSplit`
+----------------------------
+
+.. autoclass:: black.trans.CustomSplit
+    :members:
 
-.. autoclass:: black.Changed
+:class:`CustomSplitMapMixin`
+-----------------------------
+
+.. autoclass:: black.trans.CustomSplitMapMixin
     :show-inheritance:
     :members:
 
-:class:`Mode`
------------------
+:class:`StringMerger`
+----------------------
 
-.. autoclass:: black.Mode
+.. autoclass:: black.trans.StringMerger
+    :show-inheritance:
+    :members:
+
+:class:`StringParenStripper`
+-----------------------------
+
+.. autoclass:: black.trans.StringParenStripper
+    :show-inheritance:
+    :members:
+
+:class:`BaseStringSplitter`
+-----------------------------
+
+.. autoclass:: black.trans.BaseStringSplitter
+    :show-inheritance:
+    :members:
+
+:class:`StringSplitter`
+-----------------------------
+
+.. autoclass:: black.trans.StringSplitter
+    :show-inheritance:
+    :members:
+
+:class:`StringParenWrapper`
+-----------------------------
+
+.. autoclass:: black.trans.StringParenWrapper
+    :show-inheritance:
+    :members:
+
+:class:`StringParser`
+-----------------------------
+
+.. autoclass:: black.trans.StringParser
+    :members:
+
+:class:`DebugVisitor`
+------------------------
+
+.. autoclass:: black.debug.DebugVisitor
+    :show-inheritance:
+    :members:
+
+:class:`Replacement`
+------------------------
+
+.. autoclass:: black.handle_ipynb_magics.Replacement
+    :members:
+
+:class:`CellMagic`
+------------------------
+
+.. autoclass:: black.handle_ipynb_magics.CellMagic
+    :members:
+
+:class:`CellMagicFinder`
+------------------------
+
+.. autoclass:: black.handle_ipynb_magics.CellMagicFinder
+    :show-inheritance:
+    :members:
+
+:class:`OffsetAndMagic`
+------------------------
+
+.. autoclass:: black.handle_ipynb_magics.OffsetAndMagic
+    :members:
+
+:class:`MagicFinder`
+------------------------
+
+.. autoclass:: black.handle_ipynb_magics.MagicFinder
+    :show-inheritance:
+    :members:
+
+:class:`Cache`
+------------------------
+
+.. autoclass:: black.cache.Cache
+    :show-inheritance:
+    :members:
+
+Enum Classes
+~~~~~~~~~~~~~
+
+Classes inherited from Python `Enum <https://docs.python.org/3/library/enum.html#enum.Enum>`_ class.
+
+:class:`Changed`
+----------------
+
+.. autoclass:: black.report.Changed
     :show-inheritance:
     :members:
 
@@ -74,3 +211,24 @@ Enums
 .. autoclass:: black.WriteBack
     :show-inheritance:
     :members:
+
+:class:`TargetVersion`
+----------------------
+
+.. autoclass:: black.mode.TargetVersion
+    :show-inheritance:
+    :members:
+
+:class:`Feature`
+------------------
+
+.. autoclass:: black.mode.Feature
+    :show-inheritance:
+    :members:
+
+:class:`Preview`
+------------------
+
+.. autoclass:: black.mode.Preview
+    :show-inheritance:
+    :members:
index aafe61e5017c8c15468437b13716b117802ad7fe..ab46ebdb628f1f7c7b69be78deb08223ffd6a5d9 100644 (file)
@@ -5,8 +5,14 @@
 
 .. currentmodule:: black
 
+.. autoexception:: black.trans.CannotTransform
+
 .. autoexception:: black.linegen.CannotSplit
 
-.. autoexception:: black.NothingChanged
+.. autoexception:: black.brackets.BracketMatchError
+
+.. autoexception:: black.report.NothingChanged
+
+.. autoexception:: black.parsing.InvalidInput
 
-.. autoexception:: black.InvalidInput
+.. autoexception:: black.mode.Deprecated
index 4353d1bf9a9dcd1448760c2552ef0af3009ffde8..dd92e37a7d4be3792ba851fb8098f9ffa7aecf9d 100644 (file)
@@ -52,7 +52,7 @@ Formatting
 
 .. autofunction:: black.reformat_one
 
-.. autofunction:: black.schedule_formatting
+.. autofunction:: black.concurrency.schedule_formatting
 
 File operations
 ---------------
@@ -94,16 +94,10 @@ Split functions
 Caching
 -------
 
-.. autofunction:: black.cache.filter_cached
+.. autofunction:: black.cache.get_cache_dir
 
 .. autofunction:: black.cache.get_cache_file
 
-.. autofunction:: black.cache.get_cache_info
-
-.. autofunction:: black.cache.read_cache
-
-.. autofunction:: black.cache.write_cache
-
 Utilities
 ---------
 
@@ -135,9 +129,9 @@ Utilities
 
 .. autofunction:: black.comments.is_fmt_on
 
-.. autofunction:: black.comments.contains_fmt_on_at_column
+.. autofunction:: black.comments.children_contains_fmt_on
 
-.. autofunction:: black.nodes.first_leaf_column
+.. autofunction:: black.nodes.first_leaf_of
 
 .. autofunction:: black.linegen.generate_trailers_to_omit
 
@@ -163,15 +157,13 @@ Utilities
 
 .. autofunction:: black.linegen.normalize_invisible_parens
 
-.. autofunction:: black.patch_click
-
 .. autofunction:: black.nodes.preceding_leaf
 
 .. autofunction:: black.re_compile_maybe_verbose
 
 .. autofunction:: black.linegen.should_split_line
 
-.. autofunction:: black.shutdown
+.. autofunction:: black.concurrency.shutdown
 
 .. autofunction:: black.strings.sub_twice
 
index f6ff4681557d5341f375982f59c1d98649cea9d3..c6163d897b6bc6c7a4b182a9e6d90930ced5a6b2 100644 (file)
@@ -3,8 +3,11 @@ Developer reference
 
 .. note::
 
-  The documentation here is quite outdated and has been neglected. Many objects worthy
-  of inclusion aren't documented. Contributions are appreciated!
+  As of June 2023, the documentation of *Black classes* and *Black exceptions*
+  has been updated to the latest available version.
+
+  The documentation of *Black functions* is quite outdated and has been neglected. Many
+  functions worthy of inclusion aren't documented. Contributions are appreciated!
 
 *Contents are subject to change.*
 
index 718ea3dc9a292a59ff7f04a23dccc1ba91272e40..02865d6f4bd136655f947b6061ed9b9d1e1b9812 100644 (file)
 # Release process
 
-_Black_ has had a lot of work automating its release process. This document sets out to
-explain what everything does and how to release _Black_ using said automation.
-
-## Cutting a Release
-
-To cut a release, you must be a _Black_ maintainer with `GitHub Release` creation
-access. Using this access, the release process is:
-
-1. Cut a new PR editing `CHANGES.md` to version the latest changes
-   1. Example PR: https://github.com/psf/black/pull/2192
-   2. Example title: `Update CHANGES.md for XX.X release`
-2. Once the release PR is merged ensure all CI passes
-   1. If not, ensure there is an Issue open for the cause of failing CI (generally we'd
-      want this fixed before cutting a release)
-3. Open `CHANGES.md` and copy the _raw markdown_ of the latest changes to use in the
-   description of the GitHub Release.
-4. Go and [cut a release](https://github.com/psf/black/releases) using the GitHub UI so
-   that all workflows noted below are triggered.
-   1. The release version and tag should be the [CalVer](https://calver.org) version
-      _Black_ used for the current release e.g. `21.6` / `21.5b1`
-   2. _Black_ uses [setuptools scm](https://pypi.org/project/setuptools-scm/) to pull
-      the current version for the package builds and release.
-5. Once the release is cut, you're basically done. It's a good practice to go and watch
-   to make sure all the [GitHub Actions](https://github.com/psf/black/actions) pass,
-   although you should receive an email to your registered GitHub email address should
-   one fail.
-   1. You should see all the release workflows and lint/unittests workflows running on
-      the new tag in the Actions UI
-
-If anything fails, please go read the respective action's log output and configuration
-file to reverse engineer your way to a fix/soluton.
+_Black_ has had a lot of work done into standardizing and automating its release
+process. This document sets out to explain how everything works and how to release
+_Black_ using said automation.
+
+## Release cadence
+
+**We aim to release whatever is on `main` every 1-2 months.** This ensures merged
+improvements and bugfixes are shipped to users reasonably quickly, while not massively
+fracturing the user-base with too many versions. This also keeps the workload on
+maintainers consistent and predictable.
+
+If there's not much new on `main` to justify a release, it's acceptable to skip a
+month's release. Ideally January releases should not be skipped because as per our
+[stability policy](labels/stability-policy), the first release in a new calendar year
+may make changes to the _stable_ style. While the policy applies to the first release
+(instead of only January releases), confining changes to the stable style to January
+will keep things predictable (and nicer) for users.
+
+Unless there is a serious regression or bug that requires immediate patching, **there
+should not be more than one release per month**. While version numbers are cheap,
+releases require a maintainer to both commit to do the actual cutting of a release, but
+also to be able to deal with the potential fallout post-release. Releasing more
+frequently than monthly nets rapidly diminishing returns.
+
+## Cutting a release
+
+**You must have `write` permissions for the _Black_ repository to cut a release.**
+
+The 10,000 foot view of the release process is that you prepare a release PR and then
+publish a [GitHub Release]. This triggers [release automation](#release-workflows) that
+builds all release artifacts and publishes them to the various platforms we publish to.
+
+To cut a release:
+
+1. Determine the release's version number
+   - **_Black_ follows the [CalVer] versioning standard using the `YY.M.N` format**
+     - So unless there already has been a release during this month, `N` should be `0`
+   - Example: the first release in January, 2022 → `22.1.0`
+1. File a PR editing `CHANGES.md` and the docs to version the latest changes
+   1. Replace the `## Unreleased` header with the version number
+   1. Remove any empty sections for the current release
+   1. (_optional_) Read through and copy-edit the changelog (eg. by moving entries,
+      fixing typos, or rephrasing entries)
+   1. Double-check that no changelog entries since the last release were put in the
+      wrong section (e.g., run `git diff <last release> CHANGES.md`)
+   1. Add a new empty template for the next release above
+      ([template below](#changelog-template))
+   1. Update references to the latest version in
+      {doc}`/integrations/source_version_control` and
+      {doc}`/usage_and_configuration/the_basics`
+   - Example PR: [GH-3139]
+1. Once the release PR is merged, wait until all CI passes
+   - If CI does not pass, **stop** and investigate the failure(s) as generally we'd want
+     to fix failing CI before cutting a release
+1. [Draft a new GitHub Release][new-release]
+   1. Click `Choose a tag` and type in the version number, then select the
+      `Create new tag: YY.M.N on publish` option that appears
+   1. Verify that the new tag targets the `main` branch
+   1. You can leave the release title blank, GitHub will default to the tag name
+   1. Copy and paste the _raw changelog Markdown_ for the current release into the
+      description box
+1. Publish the GitHub Release, triggering [release automation](#release-workflows) that
+   will handle the rest
+1. At this point, you're basically done. It's good practice to go and [watch and verify
+   that all the release workflows pass][black-actions], although you will receive a
+   GitHub notification should something fail.
+   - If something fails, don't panic. Please go read the respective workflow's logs and
+     configuration file to reverse-engineer your way to a fix/solution.
+
+Congratulations! You've successfully cut a new release of _Black_. Go and stand up and
+take a break, you deserve it.
+
+```{important}
+Once the release artifacts reach PyPI, you may see new issues being filed indicating
+regressions. While regressions are not great, they don't automatically mean a hotfix
+release is warranted. Unless the regressions are serious and impact many users, a hotfix
+release is probably unnecessary.
+
+In the end, use your best judgement and ask other maintainers for their thoughts.
+```
+
+### Changelog template
+
+Use the following template for a clean changelog after the release:
+
+```
+## Unreleased
+
+### Highlights
+
+<!-- Include any especially major or disruptive changes here -->
+
+### Stable style
+
+<!-- Changes that affect Black's stable style -->
+
+### Preview style
+
+<!-- Changes that affect Black's preview style -->
+
+### Configuration
+
+<!-- Changes to how Black can be configured -->
+
+### Packaging
+
+<!-- Changes to how Black is packaged, such as dependency requirements -->
+
+### Parser
+
+<!-- Changes to the parser or to version autodetection -->
+
+### Performance
+
+<!-- Changes that improve Black's performance. -->
+
+### Output
+
+<!-- Changes to Black's terminal output and error messages -->
+
+### _Blackd_
+
+<!-- Changes to blackd -->
+
+### Integrations
+
+<!-- For example, Docker, GitHub Actions, pre-commit, editors -->
+
+### Documentation
+
+<!-- Major changes to documentation and policies. Small docs changes
+     don't need a changelog entry. -->
+```
 
 ## Release workflows
 
-All _Blacks_'s automation workflows use GitHub Actions. All workflows are therefore
-configured using `.yml` files in the `.github/workflows` directory of the _Black_
+All of _Black_'s release automation uses [GitHub Actions]. All workflows are therefore
+configured using YAML files in the `.github/workflows` directory of the _Black_
 repository.
 
+They are triggered by the publication of a [GitHub Release].
+
 Below are descriptions of our release workflows.
 
-### Docker
+### Publish to PyPI
+
+This is our main workflow. It builds an [sdist] and [wheels] to upload to PyPI where the
+vast majority of users will download Black from. It's divided into three job groups:
+
+#### sdist + pure wheel
+
+This single job builds the sdist and pure Python wheel (i.e., a wheel that only contains
+Python code) using [build] and then uploads them to PyPI using [twine]. These artifacts
+are general-purpose and can be used on basically any platform supported by Python.
 
-This workflow uses the QEMU powered `buildx` feature of docker to upload a `arm64` and
-`amd64`/`x86_64` build of the official _Black_ docker image™.
+#### mypyc wheels (…)
 
-- Currently this workflow uses an API Token associated with @cooperlees account
+We use [mypyc] to compile _Black_ into a CPython C extension for significantly improved
+performance. Wheels built with mypyc are platform and Python version specific.
+[Supported platforms are documented in the FAQ](labels/mypyc-support).
 
-### pypi_upload
+These matrix jobs use [cibuildwheel] which handles the complicated task of building C
+extensions for many environments for us. Since building these wheels is slow, there are
+multiple mypyc wheels jobs (hence the term "matrix") that build for a specific platform
+(as noted in the job name in parentheses).
 
-This workflow builds a Python
-[sdist](https://docs.python.org/3/distutils/sourcedist.html) and
-[wheel](https://pythonwheels.com) using the latest
-[setuptools](https://pypi.org/project/setuptools/) and
-[wheel](https://pypi.org/project/wheel/) modules.
+Like the previous job group, the built wheels are uploaded to PyPI using [twine].
 
-It will then use [twine](https://pypi.org/project/twine/) to upload both release formats
-to PyPI for general downloading of the _Black_ Python package. This is where
-[pip](https://pypi.org/project/pip/) looks by default.
+#### Update stable branch
 
-- Currently this workflow uses an API token associated with @ambv's PyPI account
+So this job doesn't _really_ belong here, but updating the `stable` branch after the
+other PyPI jobs pass (they must pass for this job to start) makes the most sense. This
+saves us from remembering to update the branch sometime after cutting the release.
 
-### Upload self-contained binaries
+- _Currently this workflow uses an API token associated with @ambv's PyPI account_
 
-This workflow builds self-contained binaries for multiple platforms. This allows people
-to download the executable for their platform and run _Black_ without a
-[Python Runtime](https://wiki.python.org/moin/PythonImplementations) installed.
+### Publish executables
 
-The created binaries are attached/stored on the associated
-[GitHub Release](https://github.com/psf/black/releases) for download over _IPv4 only_
-(GitHub still does not have IPv6 access 😢).
+This workflow builds native executables for multiple platforms using [PyInstaller]. This
+allows people to download the executable for their platform and run _Black_ without a
+[Python runtime](https://wiki.python.org/moin/PythonImplementations) installed.
 
-## Moving the `stable` tag
+The created binaries are stored on the associated GitHub Release for download over _IPv4
+only_ (GitHub still does not have IPv6 access 😢).
 
-_Black_ provides a stable tag for people who want to move along as _Black_ developers
-deem the newest version reliable. Here the _Black_ developers will move once the release
-has been problem free for at least ~24 hours from release. Given the large _Black_
-userbase we hear about bad bugs quickly. We do strive to continually improve our CI too.
+### docker
 
-### Tag moving process
+This workflow uses the QEMU powered `buildx` feature of Docker to upload an `arm64` and
+`amd64`/`x86_64` build of the official _Black_ Docker image™.
 
-#### stable
+- _Currently this workflow uses an API Token associated with @cooperlees account_
 
-From a rebased `main` checkout:
+```{note}
+This also runs on each push to `main`.
+```
 
-1. `git tag -f stable VERSION_TAG`
-   1. e.g. `git tag -f stable 21.5b1`
-1. `git push --tags -f`
+[black-actions]: https://github.com/psf/black/actions
+[build]: https://pypa-build.readthedocs.io/
+[calver]: https://calver.org
+[cibuildwheel]: https://cibuildwheel.readthedocs.io/
+[gh-3139]: https://github.com/psf/black/pull/3139
+[github actions]: https://github.com/features/actions
+[github release]: https://github.com/psf/black/releases
+[new-release]: https://github.com/psf/black/releases/new
+[mypyc]: https://mypyc.readthedocs.io/
+[mypyc-platform-support]:
+  /faq.html#what-is-compiled-yes-no-all-about-in-the-version-output
+[pyinstaller]: https://www.pyinstaller.org/
+[sdist]:
+  https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist
+[twine]: https://github.com/features/actions
+[wheels]: https://packaging.python.org/en/latest/glossary/#term-Wheel
index d61f3ec45b6edd3a2fc0d62ea21284158596fa7c..bc1680eecfd835e92fbffa3fd99cac39c9856ed0 100644 (file)
@@ -4,26 +4,20 @@ An overview on contributing to the _Black_ project.
 
 ## Technicalities
 
-Development on the latest version of Python is preferred. As of this writing it's 3.9.
-You can use any operating system.
+Development on the latest version of Python is preferred. You can use any operating
+system.
 
-Install all development dependencies using:
+Install development dependencies inside a virtual environment of your choice, for
+example:
 
 ```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.
-
-Non pipenv install works too:
+$ python3 -m venv .venv
+$ source .venv/bin/activate # activation for linux and mac
+$ .venv\Scripts\activate # activation for windows
 
-```console
-$ pip install -r test_requirements.txt
-$ pip install -e .[d]
+(.venv)$ pip install -r test_requirements.txt
+(.venv)$ pip install -e .[d]
+(.venv)$ pre-commit install
 ```
 
 Before submitting pull requests, run lints and tests with the following commands from
@@ -31,18 +25,75 @@ the root of the black repo:
 
 ```console
 # Linting
-$ pre-commit run -a
+(.venv)$ pre-commit run -a
 
 # Unit tests
-$ tox -e py
+(.venv)$ tox -e py
 
 # Optional Fuzz testing
-$ tox -e fuzz
+(.venv)$ tox -e fuzz
+
+# Format Black itself
+(.venv)$ tox -e run_self
+```
+
+### Development
+
+Further examples of invoking the tests
+
+```console
+# Run all of the above mentioned, in parallel
+(.venv)$ tox --parallel=auto
+
+# Run tests on a specific python version
+(.venv)$ tox -e py39
+
+# pass arguments to pytest
+(.venv)$ tox -e py -- --no-cov
 
-# Optional CI run to test your changes on many popular python projects
-$ black-primer [-k -w /tmp/black_test_repos]
+# print full tree diff, see documentation below
+(.venv)$ tox -e py -- --print-full-tree
+
+# disable diff printing, see documentation below
+(.venv)$ tox -e py -- --print-tree-diff=False
 ```
 
+### Testing
+
+All aspects of the _Black_ style should be tested. Normally, tests should be created as
+files in the `tests/data/cases` directory. These files consist of up to three parts:
+
+- A line that starts with `# flags: ` followed by a set of command-line options. For
+  example, if the line is `# flags: --preview --skip-magic-trailing-comma`, the test
+  case will be run with preview mode on and the magic trailing comma off. The options
+  accepted are mostly a subset of those of _Black_ itself, except for the
+  `--minimum-version=` flag, which should be used when testing a grammar feature that
+  works only in newer versions of Python. This flag ensures that we don't try to
+  validate the AST on older versions and tests that we autodetect the Python version
+  correctly when the feature is used. For the exact flags accepted, see the function
+  `get_flags_parser` in `tests/util.py`. If this line is omitted, the default options
+  are used.
+- A block of Python code used as input for the formatter.
+- The line `# output`, followed by the output of _Black_ when run on the previous block.
+  If this is omitted, the test asserts that _Black_ will leave the input code unchanged.
+
+_Black_ has two pytest command-line options affecting test files in `tests/data/` that
+are split into an input part, and an output part, separated by a line with`# output`.
+These can be passed to `pytest` through `tox`, or directly into pytest if not using
+`tox`.
+
+#### `--print-full-tree`
+
+Upon a failing test, print the full concrete syntax tree (CST) as it is after processing
+the input ("actual"), and the tree that's yielded after parsing the output ("expected").
+Note that a test can fail with different output with the same CST. This used to be the
+default, but now defaults to `False`.
+
+#### `--print-tree-diff`
+
+Upon a failing test, print the diff of the trees as described above. This is the
+default. To turn it off pass `--print-tree-diff=False`.
+
 ### News / Changelog Requirement
 
 `Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If
@@ -63,30 +114,20 @@ go back and workout what to add to the `CHANGES.md` for each release.
 
 If a change would affect the advertised code style, please modify the documentation (The
 _Black_ code style) to reflect that change. Patches that fix unintended bugs in
-formatting don't need to be mentioned separately though.
+formatting don't need to be mentioned separately though. If the change is implemented
+with the `--preview` flag, please include the change in the future style document
+instead and write the changelog entry under a dedicated "Preview changes" heading.
 
 ### Docs Testing
 
 If you make changes to docs, you can test they still build locally too.
 
 ```console
-$ pip install -r docs/requirements.txt
-$ pip install [-e] .[d]
-$ sphinx-build -a -b html -W docs/ docs/_build/
+(.venv)$ pip install -r docs/requirements.txt
+(.venv)$ pip install -e .[d]
+(.venv)$ sphinx-build -a -b html -W docs/ docs/_build/
 ```
 
-## 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/main/src/black_primer/primer.json)
-configuration for it to pass.
-
-For more `black-primer` information visit the
-[documentation](./gauging_changes.md#black-primer).
-
 ## Hygiene
 
 If you're fixing a bug, add a test. Run it first to confirm it fails, then fix the bug,
index c361addf7ae2b21b69ef1860824c1e02e12f0887..c62e1b504b52c2486f2aa643cb88679efeac1634 100644 (file)
@@ -5,20 +5,32 @@ The most common questions and issues users face are aggregated to this FAQ.
 ```{contents}
 :local:
 :backlinks: none
+:class: this-will-duplicate-information-and-it-is-still-useful-here
 ```
 
+## Why spaces? I prefer tabs
+
+PEP 8 recommends spaces over tabs, and they are used by most of the Python community.
+_Black_ provides no options to configure the indentation style, and requests for such
+options will not be considered.
+
+However, we recognise that using tabs is an accessibility issue as well. While the
+option will never be added to _Black_, visually impaired developers may find conversion
+tools such as `expand/unexpand` (for Linux) useful when contributing to Python projects.
+A workflow might consist of e.g. setting up appropriate pre-commit and post-merge git
+hooks, and scripting `unexpand` to run after applying _Black_.
+
 ## Does Black have an API?
 
 Not yet. _Black_ is fundamentally a command line tool. Many
-[integrations](integrations/index.rst) are provided, but a Python interface is not one
+[integrations](/integrations/index.md) are provided, but a Python interface is not one
 of them. A simple API is being [planned](https://github.com/psf/black/issues/779)
 though.
 
 ## Is Black safe to use?
 
-Yes, for the most part. _Black_ is strictly about formatting, nothing else. But because
-_Black_ is still in [beta](index.rst), some edges are still a bit rough. To combat
-issues, the equivalence of code after formatting is
+Yes. _Black_ is strictly about formatting, nothing else. Black strives to ensure that
+after formatting the AST is
 [checked](the_black_code_style/current_style.md#ast-before-and-after-formatting) with
 limited special cases where the code is allowed to differ. If issues are found, an error
 is raised and the file is left untouched. Magical comments that influence linters and
@@ -26,10 +38,12 @@ other tools, such as `# noqa`, may be moved by _Black_. See below for more detai
 
 ## How stable is Black's style?
 
-Quite stable. _Black_ aims to enforce one style and one style only, with some room for
-pragmatism. However, _Black_ is still in beta so style changes are both planned and
-still proposed on the issue tracker. See
-[The Black Code Style](the_black_code_style/index.rst) for more details.
+Stable. _Black_ aims to enforce one style and one style only, with some room for
+pragmatism. See [The Black Code Style](the_black_code_style/index.md) for more details.
+
+Starting in 2022, the formatting output will be stable for the releases made in the same
+year (other than unintentional bugs). It is possible to opt-in to the latest formatting
+styles, using the `--preview` flag.
 
 ## Why is my file not formatted?
 
@@ -43,6 +57,8 @@ _Black_ is timid about formatting Jupyter Notebooks. Cells containing any of the
 following will not be formatted:
 
 - automagics (e.g. `pip install black`)
+- non-Python cell magics (e.g. `%%writefile`). These can be added with the flag
+  `--python-cell-magics`, e.g. `black --python-cell-magics writefile hello.ipynb`.
 - multiline magics, e.g.:
 
   ```python
@@ -68,12 +84,18 @@ readability because operators are misaligned. Disable W503 and enable the
 disabled-by-default counterpart W504. E203 should be disabled while changes are still
 [discussed](https://github.com/PyCQA/pycodestyle/issues/373).
 
-## Does Black support Python 2?
+## Which Python versions does Black support?
 
-For formatting, yes! [Install](getting_started.md#installation) with the `python2` extra
-to format Python 2 files too! There are no current plans to drop support, but most
-likely it is bound to happen. Sometime. Eventually. In terms of running _Black_ though,
-Python 3.6 or newer is required.
+Currently the runtime requires Python 3.8-3.11. Formatting is supported for files
+containing syntax from Python 3.3 to 3.11. We promise to support at least all Python
+versions that have not reached their end of life. This is the case for both running
+_Black_ and formatting code.
+
+Support for formatting Python 2 code was removed in version 22.0. While we've made no
+plans to stop supporting older Python 3 minor versions immediately, their support might
+also be removed some time in the future without a deprecation period.
+
+Runtime support for 3.7 was removed in version 23.7.0.
 
 ## Why does my linter or typechecker complain after I format my code?
 
@@ -82,3 +104,35 @@ influence their behavior. While Black does its best to recognize such comments a
 them in the right place, this detection is not and cannot be perfect. Therefore, you'll
 sometimes have to manually move these comments to the right place after you format your
 codebase with _Black_.
+
+## Can I run Black with PyPy?
+
+Yes, there is support for PyPy 3.8 and higher.
+
+## Why does Black not detect syntax errors in my code?
+
+_Black_ is an autoformatter, not a Python linter or interpreter. Detecting all syntax
+errors is not a goal. It can format all code accepted by CPython (if you find an example
+where that doesn't hold, please report a bug!), but it may also format some code that
+CPython doesn't accept.
+
+(labels/mypyc-support)=
+
+## What is `compiled: yes/no` all about in the version output?
+
+While _Black_ is indeed a pure Python project, we use [mypyc] to compile _Black_ into a
+C Python extension, usually doubling performance. These compiled wheels are available
+for 64-bit versions of Windows, Linux (via the manylinux standard), and macOS across all
+supported CPython versions.
+
+Platforms including musl-based and/or ARM Linux distributions, and ARM Windows are
+currently **not** supported. These platforms will fall back to the slower pure Python
+wheel available on PyPI.
+
+If you are experiencing exceptionally weird issues or even segfaults, you can try
+passing `--no-binary black` to your pip install invocation. This flag excludes all
+wheels (including the pure Python wheel), so this command will use the [sdist].
+
+[mypyc]: https://mypyc.readthedocs.io/en/latest/
+[sdist]:
+  https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist
index c79dc607c4afc6aa744b104f7c9002d62888f6c9..15b7646a5094519ca1c8dfc8f8efc1191d123a6e 100644 (file)
@@ -16,14 +16,12 @@ Also, you can try out _Black_ online for minimal fuss on the
 
 ## Installation
 
-_Black_ can be installed by running `pip install black`. It requires Python 3.6.2+ to
-run, but can format Python 2 code too. Python 2 support needs the `typed_ast`
-dependency, which be installed with `pip install black[python2]`. If you want to format
-Jupyter Notebooks, install with `pip install black[jupyter]`.
+_Black_ can be installed by running `pip install black`. It requires Python 3.8+ to run.
+If you want to format Jupyter Notebooks, install with `pip install "black[jupyter]"`.
 
 If you can't wait for the latest _hotness_ and want to install from GitHub, use:
 
-`pip install git+git://github.com/psf/black`
+`pip install git+https://github.com/psf/black`
 
 ## Basic usage
 
diff --git a/.vim/bundle/black/docs/guides/index.md b/.vim/bundle/black/docs/guides/index.md
new file mode 100644 (file)
index 0000000..127279b
--- /dev/null
@@ -0,0 +1,16 @@
+# Guides
+
+```{toctree}
+---
+hidden:
+---
+
+introducing_black_to_your_project
+using_black_with_other_tools
+```
+
+Wondering how to do something specific? You've found the right place! Listed below are
+topic specific guides available:
+
+- {doc}`introducing_black_to_your_project`
+- {doc}`using_black_with_other_tools`
diff --git a/.vim/bundle/black/docs/guides/index.rst b/.vim/bundle/black/docs/guides/index.rst
deleted file mode 100644 (file)
index 717c5c4..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-Guides
-======
-
-.. toctree::
-  :hidden:
-
-  introducing_black_to_your_project
-  using_black_with_other_tools
-
-Wondering how to do something specific? You've found the right place! Listed below
-are topic specific guides available:
-
-- :doc:`introducing_black_to_your_project`
-- :doc:`using_black_with_other_tools`
index 71ccf7c114cc14ceb4f96bfc8a3e7d8ea6e4c419..71a566fbda1ad60d8099d7fafaf812398ab7abcd 100644 (file)
@@ -43,8 +43,9 @@ call to `git blame`.
 $ 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!)
+**The one caveat is that some online Git-repositories like GitLab do not yet support
+ignoring revisions using their native blame UI.** 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)).
+[GitHub supports `.git-blame-ignore-revs`](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view)
+by default in blame views however.
index 09421819ec3b50ef6e9dfe1e983c828deb7e8066..22c641a74203defdc0d9aef93d36a5c3793b49f5 100644 (file)
@@ -51,9 +51,9 @@ line_length = 88
 
 #### Why those options above?
 
-_Black_ wraps imports that surpass `line-length` by moving identifiers into their own
-indented line. If that still doesn't fit the bill, it will put all of them in separate
-lines and put a trailing comma. A more detailed explanation of this behaviour can be
+_Black_ wraps imports that surpass `line-length` by moving identifiers onto separate
+lines and by adding a trailing comma after each. A more detailed explanation of this
+behaviour can be
 [found here](../the_black_code_style/current_style.md#how-black-wraps-lines).
 
 isort's default mode of wrapping imports that extend past the `line_length` limit is
@@ -97,7 +97,7 @@ does not break older versions so you can keep it if you are running previous ver
 <details>
 <summary>.isort.cfg</summary>
 
-```cfg
+```ini
 [settings]
 profile = black
 ```
@@ -107,7 +107,7 @@ profile = black
 <details>
 <summary>setup.cfg</summary>
 
-```cfg
+```ini
 [isort]
 profile = black
 ```
@@ -173,7 +173,7 @@ limit of `88`, _Black_'s default. This explains `max-line-length = 88`.
 ```ini
 [flake8]
 max-line-length = 88
-extend-ignore = E203
+extend-ignore = E203, E704
 ```
 
 </details>
@@ -181,7 +181,7 @@ extend-ignore = E203
 <details>
 <summary>setup.cfg</summary>
 
-```cfg
+```ini
 [flake8]
 max-line-length = 88
 extend-ignore = E203
@@ -210,31 +210,16 @@ mixed feelings about _Black_'s formatting style.
 #### Configuration
 
 ```
-disable = C0330, C0326
 max-line-length = 88
 ```
 
 #### Why those options above?
 
-When _Black_ is folding very long expressions, the closing brackets will
-[be dedented](../the_black_code_style/current_style.md#how-black-wraps-lines).
+Pylint should be configured to only complain about lines that surpass `88` characters
+via `max-line-length = 88`.
 
-```py3
-ImportantClass.important_method(
-    exc, limit, lookup_lines, capture_locals, callback
-)
-```
-
-Although this style is PEP 8 compliant, Pylint will raise
-`C0330: Wrong hanging indentation before block (add 4 spaces)` warnings. Since _Black_
-isn't configurable on this style, Pylint should be told to ignore these warnings via
-`disable = C0330`.
-
-Also, since _Black_ deals with whitespace around operators and brackets, Pylint's
-warning `C0326: Bad whitespace` should be disabled using `disable = C0326`.
-
-And as usual, Pylint should be configured to only complain about lines that surpass `88`
-characters via `max-line-length = 88`.
+If using `pylint<2.6.0`, also disable `C0326` and `C0330` as these are incompatible with
+_Black_ formatting and have since been removed.
 
 #### Formats
 
@@ -242,9 +227,6 @@ characters via `max-line-length = 88`.
 <summary>pylintrc</summary>
 
 ```ini
-[MESSAGES CONTROL]
-disable = C0330, C0326
-
 [format]
 max-line-length = 88
 ```
@@ -257,9 +239,6 @@ max-line-length = 88
 ```cfg
 [pylint]
 max-line-length = 88
-
-[pylint.messages_control]
-disable = C0330, C0326
 ```
 
 </details>
@@ -268,11 +247,40 @@ disable = C0330, C0326
 <summary>pyproject.toml</summary>
 
 ```toml
-[tool.pylint.messages_control]
-disable = "C0330, C0326"
-
 [tool.pylint.format]
 max-line-length = "88"
 ```
 
 </details>
+
+### pycodestyle
+
+[pycodestyle](https://pycodestyle.pycqa.org/) is also a code linter like Flake8.
+
+#### Configuration
+
+```
+max-line-length = 88
+ignore = E203
+```
+
+#### Why those options above?
+
+pycodestyle should be configured to only complain about lines that surpass `88`
+characters via `max_line_length = 88`.
+
+See
+[Why are Flake8’s E203 and W503 violated?](https://black.readthedocs.io/en/stable/faq.html#why-are-flake8-s-e203-and-w503-violated)
+
+#### Formats
+
+<details>
+<summary>setup.cfg</summary>
+
+```cfg
+[pycodestyle]
+ignore = E203
+max_line_length = 88
+```
+
+</details>
diff --git a/.vim/bundle/black/docs/index.md b/.vim/bundle/black/docs/index.md
new file mode 100644 (file)
index 0000000..49a44ec
--- /dev/null
@@ -0,0 +1,139 @@
+<!--
+black documentation master file, created by
+sphinx-quickstart on Fri Mar 23 10:53:30 2018.
+-->
+
+# The uncompromising code formatter
+
+> “Any color you like.”
+
+By using _Black_, you agree to cede control over minutiae of hand-formatting. In return,
+_Black_ gives you speed, determinism, and freedom from `pycodestyle` nagging about
+formatting. You will save time and mental energy for more important matters.
+
+_Black_ makes code review faster by producing the smallest diffs possible. Blackened
+code looks the same regardless of the project you're reading. Formatting becomes
+transparent after a while and you can focus on the content instead.
+
+Try it out now using the [Black Playground](https://black.vercel.app).
+
+```{admonition} Note - Black is now stable!
+*Black* is [successfully used](https://github.com/psf/black#used-by) by
+many projects, small and big. *Black* has a comprehensive test suite, with efficient
+parallel tests, our own auto formatting and parallel Continuous Integration runner.
+Now that we have become stable, you should not expect large changes to formatting in
+the future. Stylistic changes will mostly be responses to bug reports and support for new Python
+syntax.
+
+Also, as a safety measure which slows down processing, *Black* will check that the
+reformatted code still produces a valid AST that is effectively equivalent to the
+original (see the
+[Pragmatism](./the_black_code_style/current_style.md#pragmatism)
+section for details). If you're feeling confident, use `--fast`.
+```
+
+```{note}
+{doc}`Black is licensed under the MIT license <license>`.
+```
+
+## Testimonials
+
+**Mike Bayer**, author of [SQLAlchemy](https://www.sqlalchemy.org/):
+
+> _I can't think of any single tool in my entire programming career that has given me a
+> bigger productivity increase by its introduction. I can now do refactorings in about
+> 1% of the keystrokes that it would have taken me previously when we had no way for
+> code to format itself._
+
+**Dusty Phillips**,
+[writer](https://smile.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords=dusty+phillips):
+
+> _Black is opinionated so you don't have to be._
+
+**Hynek Schlawack**, creator of [attrs](https://www.attrs.org/), core developer of
+Twisted and CPython:
+
+> _An auto-formatter that doesn't suck is all I want for Xmas!_
+
+**Carl Meyer**, [Django](https://www.djangoproject.com/) core developer:
+
+> _At least the name is good._
+
+**Kenneth Reitz**, creator of [requests](http://python-requests.org/) and
+[pipenv](https://docs.pipenv.org/):
+
+> _This vastly improves the formatting of our code. Thanks a ton!_
+
+## 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:
+
+```rst
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+   :target: https://github.com/psf/black
+```
+
+Looks like this:
+
+```{image} https://img.shields.io/badge/code%20style-black-000000.svg
+:target: https://github.com/psf/black
+```
+
+## Contents
+
+```{toctree}
+---
+maxdepth: 3
+includehidden:
+---
+
+the_black_code_style/index
+```
+
+```{toctree}
+---
+maxdepth: 3
+includehidden:
+caption: User Guide
+---
+
+getting_started
+usage_and_configuration/index
+integrations/index
+guides/index
+faq
+```
+
+```{toctree}
+---
+maxdepth: 2
+includehidden:
+caption: Development
+---
+
+contributing/index
+change_log
+authors
+```
+
+```{toctree}
+---
+hidden:
+caption: Project Links
+---
+
+GitHub <https://github.com/psf/black>
+PyPI <https://pypi.org/project/black>
+Chat <https://discord.gg/RtVdv86PrH>
+```
+
+# Indices and tables
+
+- {ref}`genindex`
+- {ref}`search`
diff --git a/.vim/bundle/black/docs/index.rst b/.vim/bundle/black/docs/index.rst
deleted file mode 100644 (file)
index 1515697..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-.. black documentation master file, created by
-   sphinx-quickstart on Fri Mar 23 10:53:30 2018.
-
-The uncompromising code formatter
-=================================
-
-By using *Black*, you agree to cede control over minutiae of
-hand-formatting. In return, *Black* gives you speed, determinism, and
-freedom from `pycodestyle` nagging about formatting. You will save time
-and mental energy for more important matters.
-
-*Black* makes code review faster by producing the smallest diffs
-possible. Blackened code looks the same regardless of the project
-you're reading. Formatting becomes transparent after a while and you
-can focus on the content instead.
-
-Try it out now using the `Black Playground <https://black.vercel.app>`_.
-
-.. admonition:: Note - this is a beta product
-
-   *Black* is already `successfully used <https://github.com/psf/black#used-by>`_ by
-   many projects, small and big. *Black* has a comprehensive test suite, with efficient
-   parallel tests, our own auto  formatting and parallel Continuous Integration runner.
-   However, *Black* is still beta. 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 safety measure which slows down processing, *Black* will check that the
-   reformatted code still produces a valid AST that is effectively equivalent to the
-   original (see the
-   `Pragmatism <./the_black_code_style/current_style.html#pragmatism>`_
-   section for details). If you're feeling confident, use ``--fast``.
-
-.. note::
-   :doc:`Black is licensed under the MIT license <license>`.
-
-Testimonials
-------------
-
-**Mike Bayer**, author of `SQLAlchemy <https://www.sqlalchemy.org/>`_:
-
-   *I can't think of any single tool in my entire programming career that has given me a
-   bigger productivity increase by its introduction. I can now do refactorings in about
-   1% of the keystrokes that it would have taken me previously when we had no way for
-   code to format itself.*
-
-**Dusty Phillips**, `writer <https://smile.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords=dusty+phillips>`_:
-
-   *Black is opinionated so you don't have to be.*
-
-**Hynek Schlawack**, creator of `attrs <https://www.attrs.org/>`_, core
-developer of Twisted and CPython:
-
-   *An auto-formatter that doesn't suck is all I want for Xmas!*
-
-**Carl Meyer**, `Django <https://www.djangoproject.com/>`_ core developer:
-
-   *At least the name is good.*
-
-**Kenneth Reitz**, creator of `requests <http://python-requests.org/>`_
-and `pipenv <https://docs.pipenv.org/>`_:
-
-   *This vastly improves the formatting of our code. Thanks a ton!*
-
-
-Show your style
----------------
-
-Use the badge in your project's README.md:
-
-.. code-block:: 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:
-
-.. code-block:: rst
-
-   .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-      :target: https://github.com/psf/black
-
-Looks like this:
-
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
-   :target: https://github.com/psf/black
-
-Contents
---------
-
-.. toctree::
-   :maxdepth: 3
-   :includehidden:
-
-   the_black_code_style/index
-
-.. toctree::
-   :maxdepth: 3
-   :includehidden:
-
-   getting_started
-   usage_and_configuration/index
-   integrations/index
-   guides/index
-   faq
-
-.. toctree::
-   :maxdepth: 3
-   :includehidden:
-
-   contributing/index
-   change_log
-   authors
-
-.. toctree::
-   :hidden:
-
-   GitHub ↪ <https://github.com/psf/black>
-   PyPI ↪ <https://pypi.org/project/black>
-   Chat ↪ <https://discord.gg/RtVdv86PrH>
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`search`
index d3be7c0ea84ddd482fdb01efb7846fb370a80057..7d056160fcbccf4d9db4fb0da250e6019313617d 100644 (file)
@@ -10,6 +10,67 @@ Options include the following:
 
 ## PyCharm/IntelliJ IDEA
 
+There are several different ways you can use _Black_ from PyCharm:
+
+1. Using the built-in _Black_ integration (PyCharm 2023.2 and later). This option is the
+   simplest to set up.
+1. As local server using the BlackConnect plugin. This option formats the fastest. It
+   spins up {doc}`Black's HTTP server </usage_and_configuration/black_as_a_server>`, to
+   avoid the startup cost on subsequent formats.
+1. As external tool.
+1. As file watcher.
+
+### Built-in _Black_ integration
+
+1. Install `black`.
+
+   ```console
+   $ pip install black
+   ```
+
+1. Go to `Preferences or Settings -> Tools -> Black` and configure _Black_ to your
+   liking.
+
+### As local server
+
+1. Install _Black_ with the `d` extra.
+
+   ```console
+   $ pip install 'black[d]'
+   ```
+
+1. Install
+   [BlackConnect IntelliJ IDEs plugin](https://plugins.jetbrains.com/plugin/14321-blackconnect).
+
+1. Open plugin configuration in PyCharm/IntelliJ IDEA
+
+   On macOS:
+
+   `PyCharm -> Preferences -> Tools -> BlackConnect`
+
+   On Windows / Linux / BSD:
+
+   `File -> Settings -> Tools -> BlackConnect`
+
+1. In `Local Instance (shared between projects)` section:
+
+   1. Check `Start local blackd instance when plugin loads`.
+   1. Press the `Detect` button near `Path` input. The plugin should detect the `blackd`
+      executable.
+
+1. In `Trigger Settings` section check `Trigger on code reformat` to enable code
+   reformatting with _Black_.
+
+1. Format the currently opened file by selecting `Code -> Reformat Code` or using a
+   shortcut.
+
+1. Optionally, to run _Black_ on every file save:
+
+   - In `Trigger Settings` section of plugin configuration check
+     `Trigger when saving changed files`.
+
+### As external tool
+
 1. Install `black`.
 
    ```console
@@ -57,29 +118,7 @@ Options include the following:
    - Alternatively, you can set a keyboard shortcut by navigating to
      `Preferences or Settings -> Keymap -> External Tools -> External Tools - Black`.
 
-1. Optionally, run _Black_ on every file save:
-
-   1. Make sure you have the
-      [File Watchers](https://plugins.jetbrains.com/plugin/7177-file-watchers) plugin
-      installed.
-   1. Go to `Preferences or Settings -> Tools -> File Watchers` and click `+` to add a
-      new watcher:
-      - Name: Black
-      - File type: Python
-      - Scope: Project Files
-      - Program: \<install_location_from_step_2>
-      - Arguments: `$FilePath$`
-      - Output paths to refresh: `$FilePath$`
-      - Working directory: `$ProjectFileDir$`
-
-   - In Advanced Options
-     - Uncheck "Auto-save edited files to trigger the watcher"
-     - Uncheck "Trigger the watcher on external changes"
-
-## Wing IDE
-
-Wing supports black via the OS Commands tool, as explained in the Wing documentation on
-[pep8 formatting](https://wingware.com/doc/edit/pep8). The detailed procedure is:
+### As file watcher
 
 1. Install `black`.
 
@@ -87,27 +126,89 @@ Wing supports black via the OS Commands tool, as explained in the Wing documenta
    $ pip install black
    ```
 
-1. Make sure it runs from the command line, e.g.
+1. Locate your `black` installation folder.
+
+   On macOS / Linux / BSD:
 
    ```console
-   $ black --help
+   $ which black
+   /usr/local/bin/black  # possible location
    ```
 
-1. In Wing IDE, activate the **OS Commands** panel and define the command **black** to
-   execute black on the currently selected file:
+   On Windows:
+
+   ```console
+   $ where black
+   %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe  # possible location
+   ```
+
+   Note that if you are using a virtual environment detected by PyCharm, this is an
+   unneeded step. In this case the path to `black` is `$PyInterpreterDirectory$/black`.
+
+1. Make sure you have the
+   [File Watchers](https://plugins.jetbrains.com/plugin/7177-file-watchers) plugin
+   installed.
+1. Go to `Preferences or Settings -> Tools -> File Watchers` and click `+` to add a new
+   watcher:
+   - Name: Black
+   - File type: Python
+   - Scope: Project Files
+   - Program: \<install_location_from_step_2>
+   - Arguments: `$FilePath$`
+   - Output paths to refresh: `$FilePath$`
+   - Working directory: `$ProjectFileDir$`
+
+- In Advanced Options
+  - Uncheck "Auto-save edited files to trigger the watcher"
+  - Uncheck "Trigger the watcher on external changes"
+
+## Wing IDE
+
+Wing IDE supports `black` via **Preference Settings** for system wide settings and
+**Project Properties** for per-project or workspace specific settings, as explained in
+the Wing documentation on
+[Auto-Reformatting](https://wingware.com/doc/edit/auto-reformatting). The detailed
+procedure is:
 
-   - Use the Tools -> OS Commands menu selection
-   - click on **+** in **OS Commands** -> New: Command line..
-     - Title: black
-     - Command Line: black %s
-     - I/O Encoding: Use Default
-     - Key Binding: F1
-     - [x] Raise OS Commands when executed
-     - [x] Auto-save files before execution
-     - [x] Line mode
+### Prerequistes
 
-1. Select a file in the editor and press **F1** , or whatever key binding you selected
-   in step 3, to reformat the file.
+- Wing IDE version 8.0+
+
+- Install `black`.
+
+  ```console
+  $ pip install black
+  ```
+
+- Make sure it runs from the command line, e.g.
+
+  ```console
+  $ black --help
+  ```
+
+### Preference Settings
+
+If you want Wing IDE to always reformat with `black` for every project, follow these
+steps:
+
+1. In menubar navigate to `Edit -> Preferences -> Editor -> Reformatting`.
+
+1. Set **Auto-Reformat** from `disable` (default) to `Line after edit` or
+   `Whole files before save`.
+
+1. Set **Reformatter** from `PEP8` (default) to `Black`.
+
+### Project Properties
+
+If you want to just reformat for a specific project and not intervene with Wing IDE
+global setting, follow these steps:
+
+1. In menubar navigate to `Project -> Project Properties -> Options`.
+
+1. Set **Auto-Reformat** from `Use Preferences setting` (default) to `Line after edit`
+   or `Whole files before save`.
+
+1. Set **Reformatter** from `Use Preferences setting` (default) to `Black`.
 
 ## Vim
 
@@ -119,22 +220,58 @@ Commands and shortcuts:
   - you can optionally pass `target_version=<version>` with the same values as in the
     command line.
 - `:BlackUpgrade` to upgrade _Black_ inside the virtualenv;
-- `:BlackVersion` to get the current version of _Black_ inside the virtualenv.
+- `:BlackVersion` to get the current version of _Black_ in use.
 
 Configuration:
 
 - `g:black_fast` (defaults to `0`)
 - `g:black_linelength` (defaults to `88`)
 - `g:black_skip_string_normalization` (defaults to `0`)
+- `g:black_skip_magic_trailing_comma` (defaults to `0`)
 - `g:black_virtualenv` (defaults to `~/.vim/black` or `~/.local/share/nvim/black`)
+- `g:black_use_virtualenv` (defaults to `1`)
+- `g:black_target_version` (defaults to `""`)
 - `g:black_quiet` (defaults to `0`)
+- `g:black_preview` (defaults to `0`)
+
+#### Installation
+
+This plugin **requires Vim 7.0+ built with Python 3.8+ support**. It needs Python 3.8 to
+be able to run _Black_ inside the Vim process which is much faster than calling an
+external command.
+
+##### `vim-plug`
 
 To install with [vim-plug](https://github.com/junegunn/vim-plug):
 
+_Black_'s `stable` branch tracks official version updates, and can be used to simply
+follow the most recent stable version.
+
 ```
 Plug 'psf/black', { 'branch': 'stable' }
 ```
 
+Another option which is a bit more explicit and offers more control is to use
+`vim-plug`'s `tag` option with a shell wildcard. This will resolve to the latest tag
+which matches the given pattern.
+
+The following matches all stable versions (see the
+[Release Process](../contributing/release_process.md) section for documentation of
+version scheme used by Black):
+
+```
+Plug 'psf/black', { 'tag': '*.*.*' }
+```
+
+and the following demonstrates pinning to a specific year's stable style (2022 in this
+case):
+
+```
+Plug 'psf/black', { 'tag': '22.*.*' }
+```
+
+##### Vundle
+
 or with [Vundle](https://github.com/VundleVim/Vundle.vim):
 
 ```
@@ -148,6 +285,14 @@ $ cd ~/.vim/bundle/black
 $ git checkout origin/stable -b stable
 ```
 
+##### Arch Linux
+
+On Arch Linux, the plugin is shipped with the
+[`python-black`](https://archlinux.org/packages/extra/any/python-black/) package, so you
+can start using it in Vim after install with no additional setup.
+
+##### Vim 8 Native Plugin Management
+
 or you can copy the plugin files from
 [plugin/black.vim](https://github.com/psf/black/blob/stable/plugin/black.vim) and
 [autoload/black.vim](https://github.com/psf/black/blob/stable/autoload/black.vim).
@@ -162,9 +307,7 @@ curl https://raw.githubusercontent.com/psf/black/stable/autoload/black.vim -o ~/
 Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or
 Pathogen, and so on.
 
-This plugin **requires Vim 7.0+ built with Python 3.6+ support**. It needs Python 3.6 to
-be able to run _Black_ inside the Vim process which is much faster than calling an
-external command.
+#### Usage
 
 On first run, the plugin creates its own virtualenv using the right Python version and
 automatically installs _Black_. You can upgrade it later by calling `:BlackUpgrade` and
@@ -174,70 +317,31 @@ If you need to do anything special to make your virtualenv work and install _Bla
 example you want to run a version from main), create a virtualenv manually and point
 `g:black_virtualenv` to it. The plugin will use it.
 
-To run _Black_ on save, add the following line to `.vimrc` or `init.vim`:
+If you would prefer to use the system installation of _Black_ rather than a virtualenv,
+then add this to your vimrc:
 
 ```
-autocmd BufWritePre *.py execute ':Black'
+let g:black_use_virtualenv = 0
 ```
 
-To run _Black_ on a key press (e.g. F9 below), add this:
+Note that the `:BlackUpgrade` command is only usable and useful with a virtualenv, so
+when the virtualenv is not in use, `:BlackUpgrade` is disabled. If you need to upgrade
+the system installation of _Black_, then use your system package manager or pip--
+whatever tool you used to install _Black_ originally.
 
-```
-nnoremap <F9> :Black<CR>
-```
+To run _Black_ on save, add the following lines to `.vimrc` or `init.vim`:
 
-**How to get Vim with Python 3.6?** On Ubuntu 17.10 Vim comes with Python 3.6 by
-default. On macOS with Homebrew run: `brew install vim`. When building Vim from source,
-use: `./configure --enable-python3interp=yes`. There's many guides online how to do
-this.
-
-**I get an import error when using _Black_ from a virtual environment**: If you get an
-error message like this:
-
-```text
-Traceback (most recent call last):
-  File "<string>", line 63, in <module>
-  File "/home/gui/.vim/black/lib/python3.7/site-packages/black.py", line 45, in <module>
-    from typed_ast import ast3, ast27
-  File "/home/gui/.vim/black/lib/python3.7/site-packages/typed_ast/ast3.py", line 40, in <module>
-    from typed_ast import _ast3
-ImportError: /home/gui/.vim/black/lib/python3.7/site-packages/typed_ast/_ast3.cpython-37m-x86_64-linux-gnu.so: undefined symbool: PyExc_KeyboardInterrupt
 ```
-
-Then you need to install `typed_ast` and `regex` directly from the source code. The
-error happens because `pip` will download [Python wheels](https://pythonwheels.com/) if
-they are available. Python wheels are a new standard of distributing Python packages and
-packages that have Cython and extensions written in C are already compiled, so the
-installation is much more faster. The problem here is that somehow the Python
-environment inside Vim does not match with those already compiled C extensions and these
-kind of errors are the result. Luckily there is an easy fix: installing the packages
-from the source code.
-
-The two packages that cause the problem are:
-
-- [regex](https://pypi.org/project/regex/)
-- [typed-ast](https://pypi.org/project/typed-ast/)
-
-Now remove those two packages:
-
-```console
-$ pip uninstall regex typed-ast -y
+augroup black_on_save
+  autocmd!
+  autocmd BufWritePre *.py Black
+augroup end
 ```
 
-And now you can install them with:
+To run _Black_ on a key press (e.g. F9 below), add this:
 
-```console
-$ pip install --no-binary :all: regex typed-ast
 ```
-
-The C extensions will be compiled and now Vim's Python environment will match. Note that
-you need to have the GCC compiler and the Python development files installed (on
-Ubuntu/Debian do `sudo apt-get install build-essential python3-dev`).
-
-If you later want to update _Black_, you should do it like this:
-
-```console
-$ pip install -U black --no-binary regex,typed-ast
+nnoremap <F9> :Black<CR>
 ```
 
 ### With ALE
@@ -285,13 +389,20 @@ close and reopen your File, _Black_ will be done with its job.
 
 ## Visual Studio Code
 
-Use the
-[Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
-([instructions](https://code.visualstudio.com/docs/python/editing#_formatting)).
+- Use the
+  [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
+  ([instructions](https://code.visualstudio.com/docs/python/formatting)).
+
+- Alternatively the pre-release
+  [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter)
+  extension can be used which runs a [Language Server Protocol](https://langserver.org/)
+  server for Black. Formatting is much more responsive using this extension, **but the
+  minimum supported version of Black is 22.3.0**.
 
-## SublimeText 3
+## SublimeText
 
-Use [sublack plugin](https://github.com/jgirardet/sublack).
+For SublimeText 3, use [sublack plugin](https://github.com/jgirardet/sublack). For
+higher versions, it is recommended to use [LSP](#python-lsp-server) as documented below.
 
 ## Python LSP Server
 
index e866a3cc616429585a8bcad69e9de10d35ebbfdc..56b2cdd05868121b1fd75a68f7040b4d35f45ba0 100644 (file)
@@ -8,8 +8,8 @@ environment. Great for enforcing that your code matches the _Black_ code style.
 This action is known to support all GitHub-hosted runner OSes. In addition, only
 published versions of _Black_ are supported (i.e. whatever is available on PyPI).
 
-Finally, this action installs _Black_ with both the `colorama` and `python2` extras so
-the `--color` flag and formatting Python 2 code are supported.
+Finally, this action installs _Black_ with the `colorama` extra so the `--color` flag
+should work fine.
 
 ## Usage
 
@@ -24,7 +24,7 @@ jobs:
   lint:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: psf/black@stable
 ```
 
@@ -32,12 +32,21 @@ We recommend the use of the `@stable` tag, but per version tags also exist if yo
 that. Note that the action's version you select is independent of the version of _Black_
 the action will use.
 
-The version of _Black_ the action will use can be configured via `version`. The action
-defaults to the latest release available on PyPI. Only versions available from PyPI are
-supported, so no commit SHAs or branch names.
+The version of _Black_ the action will use can be configured via `version`. This can be
+any
+[valid version specifier](https://packaging.python.org/en/latest/glossary/#term-Version-Specifier)
+or just the version number if you want an exact version. The action defaults to the
+latest release available on PyPI. Only versions available from PyPI are supported, so no
+commit SHAs or branch names.
+
+If you want to include Jupyter Notebooks, _Black_ must be installed with the `jupyter`
+extra. Installing the extra and including Jupyter Notebook files can be configured via
+`jupyter` (default is `false`).
 
 You can also configure the arguments passed to _Black_ via `options` (defaults to
-`'--check --diff'`) and `src` (default is `'.'`)
+`'--check --diff'`) and `src` (default is `'.'`). Please note that the
+[`--check` flag](labels/exit-code) is required so that the workflow fails if _Black_
+finds files that need to be formatted.
 
 Here's an example configuration:
 
@@ -46,5 +55,18 @@ Here's an example configuration:
   with:
     options: "--check --verbose"
     src: "./src"
+    jupyter: true
     version: "21.5b1"
 ```
+
+If you want to match versions covered by Black's
+[stability policy](labels/stability-policy), you can use the compatible release operator
+(`~=`):
+
+```yaml
+- uses: psf/black@stable
+  with:
+    options: "--check --verbose"
+    src: "./src"
+    version: "~= 22.0"
+```
diff --git a/.vim/bundle/black/docs/integrations/index.md b/.vim/bundle/black/docs/integrations/index.md
new file mode 100644 (file)
index 0000000..33135d0
--- /dev/null
@@ -0,0 +1,31 @@
+# Integrations
+
+```{toctree}
+---
+hidden:
+---
+
+editors
+github_actions
+source_version_control
+```
+
+_Black_ can be integrated into many environments, providing a better and smoother
+experience. Documentation for integrating _Black_ with a tool can be found for the
+following areas:
+
+- {doc}`Editor / IDE <./editors>`
+- {doc}`GitHub Actions <./github_actions>`
+- {doc}`Source version control <./source_version_control>`
+
+Editors and tools not listed will require external contributions.
+
+Patches welcome! ✨ 🍰 ✨
+
+Any tool can pipe code through _Black_ using its stdio mode (just
+[use `-` as the file name](https://www.tldp.org/LDP/abs/html/special-chars.html#DASHREF2)).
+The formatted code will be returned on stdout (unless `--check` was passed). _Black_
+will still emit messages on stderr but that shouldn't affect your use case.
+
+This can be used for example with PyCharm's or IntelliJ's
+[File Watchers](https://www.jetbrains.com/help/pycharm/file-watchers.html).
diff --git a/.vim/bundle/black/docs/integrations/index.rst b/.vim/bundle/black/docs/integrations/index.rst
deleted file mode 100644 (file)
index ed62ebc..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-Integrations
-============
-
-.. toctree::
-    :hidden:
-
-    editors
-    github_actions
-    source_version_control
-
-*Black* can be integrated into many environments, providing a better and smoother experience. Documentation for integrating *Black* with a tool can be found for the
-following areas:
-
-- :doc:`Editor / IDE <./editors>`
-- :doc:`GitHub Actions <./github_actions>`
-- :doc:`Source version control <./source_version_control>`
-
-Editors and tools not listed will require external contributions.
-
-Patches welcome! ✨ 🍰 ✨
-
-Any tool can pipe code through *Black* using its stdio mode (just
-`use \`-\` as the file name <https://www.tldp.org/LDP/abs/html/special-chars.html#DASHREF2>`_).
-The formatted code will be returned on stdout (unless ``--check`` was passed). *Black*
-will still emit messages on stderr but that shouldn't affect your use case.
-
-This can be used for example with PyCharm's or IntelliJ's
-`File Watchers <https://www.jetbrains.com/help/pycharm/file-watchers.html>`_.
index 6a1aa363d2be3e009e5652a9acad9e5b241a3da7..16354f849ba90eacc29b2028b8e1aaa4f7f95733 100644 (file)
@@ -6,26 +6,48 @@ Use [pre-commit](https://pre-commit.com/). Once you
 
 ```yaml
 repos:
-  - repo: https://github.com/psf/black
-    rev: 21.9b0
+  # Using this mirror lets us use mypyc-compiled black, which is about 2x faster
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 23.10.0
     hooks:
       - id: black
         # It is recommended to specify the latest version of Python
         # supported by your project here, or alternatively use
         # pre-commit's default_language_version, see
         # https://pre-commit.com/#top_level-default_language_version
-        language_version: python3.9
+        language_version: python3.11
 ```
 
-Feel free to switch out the `rev` value to something else, like another
-[tag/version][black-tags] or even a specific commit. Although we discourage the use of
+Feel free to switch out the `rev` value to a different version of Black.
+
+Note if you'd like to use a specific commit in `rev`, you'll need to swap the repo
+specified from the mirror to https://github.com/psf/black. We discourage the use of
 branches or other mutable refs since the hook [won't auto update as you may
 expect][pre-commit-mutable-rev].
 
-If you want support for Jupyter Notebooks as well, then replace `id: black` with
-`id: black-jupyter` (though note that it's only available from version `21.8b0`
-onwards).
+## Jupyter Notebooks
+
+There is an alternate hook `black-jupyter` that expands the targets of `black` to
+include Jupyter Notebooks. To use this hook, simply replace the hook's `id: black` with
+`id: black-jupyter` in the `.pre-commit-config.yaml`:
+
+```yaml
+repos:
+  # Using this mirror lets us use mypyc-compiled black, which is about 2x faster
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 23.10.0
+    hooks:
+      - id: black-jupyter
+        # It is recommended to specify the latest version of Python
+        # supported by your project here, or alternatively use
+        # pre-commit's default_language_version, see
+        # https://pre-commit.com/#top_level-default_language_version
+        language_version: python3.11
+```
+
+```{note}
+The `black-jupyter` hook became available in version 21.8b0.
+```
 
-[black-tags]: https://github.com/psf/black/tags
 [pre-commit-mutable-rev]:
   https://pre-commit.com/#using-the-latest-version-for-a-repository
diff --git a/.vim/bundle/black/docs/license.md b/.vim/bundle/black/docs/license.md
new file mode 100644 (file)
index 0000000..132c95b
--- /dev/null
@@ -0,0 +1,9 @@
+---
+orphan: true
+---
+
+# License
+
+```{include} ../LICENSE
+
+```
diff --git a/.vim/bundle/black/docs/license.rst b/.vim/bundle/black/docs/license.rst
deleted file mode 100644 (file)
index 2dc20a2..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-:orphan:
-
-License
-=======
-
-.. include:: ../LICENSE
index 4c5b700412a502722c06b64529556e84241238e9..b5b9e22fc84416897d176ee81314177668baf71a 100644 (file)
@@ -1,6 +1,9 @@
 # Used by ReadTheDocs; pinned requirements for stability.
 
-myst-parser==0.15.1
-Sphinx==4.1.2
+myst-parser==2.0.0
+Sphinx==7.2.6
+# Older versions break Sphinx even though they're declared to be supported.
+docutils==0.20.1
 sphinxcontrib-programoutput==0.17
-sphinx_copybutton==0.4.0
+sphinx_copybutton==0.5.2
+furo==2023.9.10
index b9ab350cd12882d6d3bd43f3fc3b010de80324f6..ff757a8276bd89dd96908857d9da4215b26cb756 100644 (file)
@@ -2,20 +2,28 @@
 
 ## Code style
 
-_Black_ reformats entire files in place. Style configuration options are deliberately
-limited and rarely added. It doesn't take previous formatting into account, except for
-the magic trailing comma and preserving newlines. It doesn't reformat blocks that start
-with `# fmt: off` and end with `# fmt: on`, or lines that ends with `# fmt: skip`.
-`# fmt: on/off` have to be on the same level of indentation. It also recognizes
+_Black_ aims for consistency, generality, readability and reducing git diffs. Similar
+language constructs are formatted with similar rules. Style configuration options are
+deliberately limited and rarely added. Previous formatting is taken into account as
+little as possible, with rare exceptions like the magic trailing comma. The coding style
+used by _Black_ can be viewed as a strict subset of PEP 8.
+
+_Black_ reformats entire files in place. It doesn't reformat lines that end with
+`# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`.
+`# fmt: on/off` must be on the same level of indentation and in the same block, meaning
+no unindents beyond the initial indentation level between them. It also recognizes
 [YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a
 courtesy for straddling code.
 
+The rest of this document describes the current formatting style. If you're interested
+in trying out where the style is heading, see [future style](./future_style.md) and try
+running `black --preview`.
+
 ### How _Black_ wraps lines
 
 _Black_ ignores previous formatting and applies uniform horizontal and vertical
 whitespace to your code. The rules for horizontal whitespace can be summarized as: do
-whatever makes `pycodestyle` happy. The coding style used by _Black_ can be viewed as a
-strict subset of PEP 8.
+whatever makes `pycodestyle` happy.
 
 As for vertical whitespace, _Black_ tries to render one full expression or simple
 statement per line. If this fits the allotted line length, great.
@@ -77,6 +85,19 @@ def very_important_function(
         ...
 ```
 
+If a data structure literal (tuple, list, set, dict) or a line of "from" imports cannot
+fit in the allotted length, it's always split into one element per line. This minimizes
+diffs as well as enables readers of code to find which commit introduced a particular
+entry. This also makes _Black_ compatible with
+[isort](../guides/using_black_with_other_tools.md#isort) with the ready-made `black`
+profile or manual configuration.
+
+You might have noticed that closing brackets are always dedented and that a trailing
+comma is always added. Such formatting produces smaller diffs; when you add or remove an
+element, it's always just one line. Also, having the closing bracket dedented provides a
+clear delimiter between two distinct sections of the code that otherwise share the same
+indentation level (like the arguments list and the docstring in the example above).
+
 (labels/why-no-backslashes)=
 
 _Black_ prefers parentheses over backslashes, and will remove backslashes if found.
@@ -119,18 +140,7 @@ If you're reaching for backslashes, that's a clear signal that you can do better
 slightly refactor your code. I hope some of the examples above show you that there are
 many ways in which you can do it.
 
-You might have noticed that closing brackets are always dedented and that a trailing
-comma is always added. Such formatting produces smaller diffs; when you add or remove an
-element, it's always just one line. Also, having the closing bracket dedented provides a
-clear delimiter between two distinct sections of the code that otherwise share the same
-indentation level (like the arguments list and the docstring in the example above).
-
-If a data structure literal (tuple, list, set, dict) or a line of "from" imports cannot
-fit in the allotted length, it's always split into one element per line. This minimizes
-diffs as well as enables readers of code to find which commit introduced a particular
-entry. This also makes _Black_ compatible with
-[isort](../guides/using_black_with_other_tools.md#isort) with the ready-made `black`
-profile or manual configuration.
+(labels/line-length)=
 
 ### Line length
 
@@ -150,33 +160,35 @@ harder to work with line lengths exceeding 100 characters. It also adversely aff
 side-by-side diff review on typical screen resolutions. Long lines also make it harder
 to present code neatly in documentation or talk slides.
 
-If you're using Flake8, you can bump `max-line-length` to 88 and mostly forget about it.
-However, it's better if you use [Bugbear](https://github.com/PyCQA/flake8-bugbear)'s
-B950 warning instead of E501, and bump the max line length to 88 (or the `--line-length`
-you used for black), which will align more with black's _"try to respect
-`--line-length`, but don't become crazy if you can't"_. You'd do it like this:
-
-```ini
-[flake8]
-max-line-length = 88
-...
-select = C,E,F,W,B,B950
-extend-ignore = E203, E501
-```
+#### Flake8
 
-Explanation of why E203 is disabled can be found further in this documentation. And if
-you're curious about the reasoning behind B950,
-[Bugbear's documentation](https://github.com/PyCQA/flake8-bugbear#opinionated-warnings)
-explains it. The tl;dr is "it's like highway speed limits, we won't bother you if you
-overdo it by a few km/h".
+If you use Flake8, you have a few options:
 
-**If you're looking for a minimal, black-compatible flake8 configuration:**
+1. Recommended is using [Bugbear](https://github.com/PyCQA/flake8-bugbear) and enabling
+   its B950 check instead of using Flake8's E501, because it aligns with Black's 10%
+   rule. Install Bugbear and use the following config:
 
-```ini
-[flake8]
-max-line-length = 88
-extend-ignore = E203
-```
+   ```ini
+   [flake8]
+   max-line-length = 80
+   ...
+   select = C,E,F,W,B,B950
+   extend-ignore = E203, E501, E704
+   ```
+
+   The rationale for E950 is explained in
+   [Bugbear's documentation](https://github.com/PyCQA/flake8-bugbear#opinionated-warnings).
+
+2. For a minimally compatible config:
+
+   ```ini
+   [flake8]
+   max-line-length = 88
+   extend-ignore = E203, E704
+   ```
+
+An explanation of why E203 is disabled can be found in the [Slices section](#slices) of
+this page.
 
 ### Empty lines
 
@@ -186,7 +198,45 @@ that in-function vertical whitespace should only be used sparingly.
 _Black_ will allow single empty lines inside functions, and single and double empty
 lines on module level left by the original editors, except when they're within
 parenthesized expressions. Since such expressions are always reformatted to fit minimal
-space, this whitespace is lost.
+space, this whitespace is lost. The other exception is that it will remove any empty
+lines immediately following a statement that introduces a new indentation level.
+
+```python
+# in:
+
+def foo():
+
+    print("All the newlines above me should be deleted!")
+
+
+if condition:
+
+    print("No newline above me!")
+
+    print("There is a newline above me, and that's OK!")
+
+
+class Point:
+
+    x: int
+    y: int
+
+# out:
+
+def foo():
+    print("All the newlines above me should be deleted!")
+
+
+if condition:
+    print("No newline above me!")
+
+    print("There is a newline above me, and that's OK!")
+
+
+class Point:
+    x: int
+    y: int
+```
 
 It will also insert proper spacing before and after function definitions. It's one line
 before and after inner functions and two lines before and after module-level functions
@@ -204,11 +254,12 @@ required due to an inner function starting immediately after.
 
 _Black_ does not format comment contents, but it enforces two spaces between code and a
 comment on the same line, and a space before the comment text begins. Some types of
-comments that require specific spacing rules are respected: doc comments (`#: comment`),
-section comments with long runs of hashes, and Spyder cells. Non-breaking spaces after
-hashes are also preserved. Comments may sometimes be moved because of formatting
-changes, which can break tools that assign special meaning to them. See
-[AST before and after formatting](#ast-before-and-after-formatting) for more discussion.
+comments that require specific spacing rules are respected: shebangs (`#! comment`), doc
+comments (`#: comment`), section comments with long runs of hashes, and Spyder cells.
+Non-breaking spaces after hashes are also preserved. Comments may sometimes be moved
+because of formatting changes, which can break tools that assign special meaning to
+them. See [AST before and after formatting](#ast-before-and-after-formatting) for more
+discussion.
 
 ### Trailing commas
 
@@ -227,16 +278,18 @@ A pre-existing trailing comma informs _Black_ to always explode contents of the
 bracket pair into one item per line. Read more about this in the
 [Pragmatism](#pragmatism) section below.
 
+(labels/strings)=
+
 ### Strings
 
 _Black_ prefers double quotes (`"` and `"""`) over single quotes (`'` and `'''`). It
 will replace the latter with the former as long as it does not result in more backslash
 escapes than before.
 
-_Black_ also standardizes string prefixes, making them always lowercase. On top of that,
-if your code is already Python 3.6+ only or it's using the `unicode_literals` future
-import, _Black_ will remove `u` from the string prefix as it is meaningless in those
-scenarios.
+_Black_ also standardizes string prefixes. Prefix characters are made lowercase with the
+exception of [capital "R" prefixes](#rstrings-and-rstrings), unicode literal markers
+(`u`) are removed because they are meaningless in Python 3, and in the case of multiple
+characters "r" is put first as in spoken language: "raw f-string".
 
 The main reason to standardize on a single form of quotes is aesthetics. Having one kind
 of quotes everywhere reduces reader distraction. It will also enable a future version of
@@ -260,16 +313,6 @@ If you are adopting _Black_ in a large project with pre-existing string conventi
 you can pass `--skip-string-normalization` on the command line. This is meant as an
 adoption helper, avoid using this for new projects.
 
-(labels/experimental-string)=
-
-As an experimental option (can be enabled by `--experimental-string-processing`),
-_Black_ splits long strings (using parentheses where appropriate) and merges short ones.
-When split, parts of f-strings that don't need formatting are converted to plain
-strings. User-made splits are respected when they do not exceed the line length limit.
-Line continuation backslashes are converted into parenthesized strings. Unnecessary
-parentheses are stripped. Because the functionality is experimental, feedback and issue
-reports are highly encouraged!
-
 _Black_ also processes docstrings. Firstly the indentation of docstrings is corrected
 for both quotations and the text within, although relative indentation in the text is
 preserved. Superfluous trailing whitespace on each line and unnecessary new lines at the
@@ -281,8 +324,7 @@ removed.
 
 _Black_ standardizes most numeric literals to use lowercase letters for the syntactic
 parts and uppercase letters for the digits themselves: `0xAB` instead of `0XAB` and
-`1e10` instead of `1E10`. Python 2 long literals are styled as `2L` instead of `2l` to
-avoid confusion between `l` and `1`.
+`1e10` instead of `1E10`.
 
 ### Line breaks & binary operators
 
@@ -291,6 +333,26 @@ multiple lines. This is so that _Black_ is compliant with the recent changes in
 [PEP 8](https://www.python.org/dev/peps/pep-0008/#should-a-line-break-before-or-after-a-binary-operator)
 style guide, which emphasizes that this approach improves readability.
 
+Almost all operators will be surrounded by single spaces, the only exceptions are unary
+operators (`+`, `-`, and `~`), and power operators when both operands are simple. For
+powers, an operand is considered simple if it's only a NAME, numeric CONSTANT, or
+attribute access (chained attribute access is allowed), with or without a preceding
+unary operator.
+
+```python
+# For example, these won't be surrounded by whitespace
+a = x**y
+b = config.base**5.2
+c = config.base**runtime.config.exponent
+d = 2**5
+e = 2**~5
+
+# ... but these will be surrounded by whitespace
+f = 2 ** get_exponent()
+g = get_x() ** get_y()
+h = config['base'] ** 2
+```
+
 ### Slices
 
 PEP 8
@@ -383,16 +445,16 @@ recommended code style for those files is more terse than PEP 8:
 _Black_ enforces the above rules. There are additional guidelines for formatting `.pyi`
 file that are not enforced yet but might be in a future version of the formatter:
 
-- all function bodies should be empty (contain `...` instead of the body);
-- do not use docstrings;
 - prefer `...` over `pass`;
-- for arguments with a default, use `...` instead of the actual default;
 - avoid using string literals in type annotations, stub files support forward references
   natively (like Python 3.7 code with `from __future__ import annotations`);
 - use variable annotations instead of type comments, even for stubs that target older
-  versions of Python;
-- for arguments that default to `None`, use `Optional[]` explicitly;
-- use `float` instead of `Union[int, float]`.
+  versions of Python.
+
+### Line endings
+
+_Black_ will normalize line endings (`\n` or `\r\n`) based on the first line ending of
+the file.
 
 ## Pragmatism
 
@@ -402,6 +464,8 @@ there were not many users anyway. Not many edge cases were reported. As a mature
 _Black_ does make some exceptions to rules it otherwise holds. This section documents
 what those exceptions are and why this is the case.
 
+(labels/magic-trailing-comma)=
+
 ### The magic trailing comma
 
 _Black_ in general does not take existing formatting into account.
@@ -438,17 +502,19 @@ default by (among others) GitHub and Visual Studio Code, differentiates between
 r-strings and R-strings. The former are syntax highlighted as regular expressions while
 the latter are treated as true raw strings with no special semantics.
 
+(labels/ast-changes)=
+
 ### AST before and after formatting
 
-When run with `--safe`, _Black_ checks that the code before and after is semantically
-equivalent. This check is done by comparing the AST of the source with the AST of the
-target. There are three limited cases in which the AST does differ:
+When run with `--safe` (the default), _Black_ checks that the code before and after is
+semantically equivalent. This check is done by comparing the AST of the source with the
+AST of the target. There are three limited cases in which the AST does differ:
 
 1. _Black_ cleans up leading and trailing whitespace of docstrings, re-indenting them if
    needed. It's been one of the most popular user-reported features for the formatter to
    fix whitespace issues with docstrings. While the result is technically an AST
-   difference, due to the various possibilities of forming docstrings, all realtime use
-   of docstrings that we're aware of sanitizes indentation and leading/trailing
+   difference, due to the various possibilities of forming docstrings, all real-world
+   uses of docstrings that we're aware of sanitize indentation and leading/trailing
    whitespace anyway.
 
 1. _Black_ manages optional parentheses for some statements. In the case of the `del`
index a7676090553a319a2f4ef003bed71634f0b17d3a..367ff98537c8ff333bd919b62d0411e5f2d9732e 100644 (file)
@@ -19,24 +19,208 @@ with make_context_manager1() as cm1, make_context_manager2() as cm2, make_contex
     ...  # nothing to split on - line too long
 ```
 
-So _Black_ will eventually format it like this:
+So _Black_ will, when we implement this, format it like this:
 
 ```py3
 with \
-     make_context_manager(1) as cm1, \
-     make_context_manager(2) as cm2, \
-     make_context_manager(3) as cm3, \
-     make_context_manager(4) as cm4 \
+     make_context_manager1() as cm1, \
+     make_context_manager2() as cm2, \
+     make_context_manager3() as cm3, \
+     make_context_manager4() as cm4 \
 :
     ...  # backslashes and an ugly stranded colon
 ```
 
-Although when the target version is Python 3.9 or higher, _Black_ will use parentheses
-instead since they're allowed in Python 3.9 and higher.
+Although when the target version is Python 3.9 or higher, _Black_ uses parentheses
+instead in `--preview` mode (see below) since they're allowed in Python 3.9 and higher.
 
-## Improved string processing
+An alternative to consider if the backslashes in the above formatting are undesirable is
+to use {external:py:obj}`contextlib.ExitStack` to combine context managers in the
+following way:
 
-Currently, _Black_ does not split long strings to fit the line length limit. Currently,
-there is [an experimental option](labels/experimental-string) to enable splitting
-strings. We plan to enable this option by default once it is fully stable. This is
+```python
+with contextlib.ExitStack() as exit_stack:
+    cm1 = exit_stack.enter_context(make_context_manager1())
+    cm2 = exit_stack.enter_context(make_context_manager2())
+    cm3 = exit_stack.enter_context(make_context_manager3())
+    cm4 = exit_stack.enter_context(make_context_manager4())
+    ...
+```
+
+(labels/preview-style)=
+
+## Preview style
+
+Experimental, potentially disruptive style changes are gathered under the `--preview`
+CLI flag. At the end of each year, these changes may be adopted into the default style,
+as described in [The Black Code Style](index.md). Because the functionality is
+experimental, feedback and issue reports are highly encouraged!
+
+### Improved string processing
+
+_Black_ will split long string literals and merge short ones. Parentheses are used where
+appropriate. When split, parts of f-strings that don't need formatting are converted to
+plain strings. User-made splits are respected when they do not exceed the line length
+limit. Line continuation backslashes are converted into parenthesized strings.
+Unnecessary parentheses are stripped. The stability and status of this feature is
 tracked in [this issue](https://github.com/psf/black/issues/2188).
+
+### Improved line breaks
+
+For assignment expressions, _Black_ now prefers to split and wrap the right side of the
+assignment instead of left side. For example:
+
+```python
+some_dict[
+    "with_a_long_key"
+] = some_looooooooong_module.some_looooooooooooooong_function_name(
+    first_argument, second_argument, third_argument
+)
+```
+
+will be changed to:
+
+```python
+some_dict["with_a_long_key"] = (
+    some_looooooooong_module.some_looooooooooooooong_function_name(
+        first_argument, second_argument, third_argument
+    )
+)
+```
+
+### Improved parentheses management
+
+For dict literals with long values, they are now wrapped in parentheses. Unnecessary
+parentheses are now removed. For example:
+
+```python
+my_dict = {
+    "a key in my dict": a_very_long_variable
+    * and_a_very_long_function_call()
+    / 100000.0,
+    "another key": (short_value),
+}
+```
+
+will be changed to:
+
+```python
+my_dict = {
+    "a key in my dict": (
+        a_very_long_variable * and_a_very_long_function_call() / 100000.0
+    ),
+    "another key": short_value,
+}
+```
+
+### Improved multiline string handling
+
+_Black_ is smarter when formatting multiline strings, especially in function arguments,
+to avoid introducing extra line breaks. Previously, it would always consider multiline
+strings as not fitting on a single line. With this new feature, _Black_ looks at the
+context around the multiline string to decide if it should be inlined or split to a
+separate line. For example, when a multiline string is passed to a function, _Black_
+will only split the multiline string if a line is too long or if multiple arguments are
+being passed.
+
+For example, _Black_ will reformat
+
+```python
+textwrap.dedent(
+    """\
+    This is a
+    multiline string
+"""
+)
+```
+
+to:
+
+```python
+textwrap.dedent("""\
+    This is a
+    multiline string
+""")
+```
+
+And:
+
+```python
+MULTILINE = """
+foobar
+""".replace(
+    "\n", ""
+)
+```
+
+to:
+
+```python
+MULTILINE = """
+foobar
+""".replace("\n", "")
+```
+
+Implicit multiline strings are special, because they can have inline comments. Strings
+without comments are merged, for example
+
+```python
+s = (
+    "An "
+    "implicit "
+    "multiline "
+    "string"
+)
+```
+
+becomes
+
+```python
+s = "An implicit multiline string"
+```
+
+A comment on any line of the string (or between two string lines) will block the
+merging, so
+
+```python
+s = (
+    "An "  # Important comment concerning just this line
+    "implicit "
+    "multiline "
+    "string"
+)
+```
+
+and
+
+```python
+s = (
+    "An "
+    "implicit "
+    # Comment in between
+    "multiline "
+    "string"
+)
+```
+
+will not be merged. Having the comment after or before the string lines (but still
+inside the parens) will merge the string. For example
+
+```python
+s = (  # Top comment
+    "An "
+    "implicit "
+    "multiline "
+    "string"
+    # Bottom comment
+)
+```
+
+becomes
+
+```python
+s = (  # Top comment
+    "An implicit multiline string"
+    # Bottom comment
+)
+```
diff --git a/.vim/bundle/black/docs/the_black_code_style/index.md b/.vim/bundle/black/docs/the_black_code_style/index.md
new file mode 100644 (file)
index 0000000..1719347
--- /dev/null
@@ -0,0 +1,52 @@
+# The Black Code Style
+
+```{toctree}
+---
+hidden:
+---
+
+Current style <current_style>
+Future style <future_style>
+```
+
+_Black_ is a PEP 8 compliant opinionated formatter with its own style.
+
+While keeping the style unchanged throughout releases has always been a goal, the
+_Black_ code style isn't set in stone. It evolves to accommodate for new features in the
+Python language and, occasionally, in response to user feedback. Large-scale style
+preferences presented in {doc}`current_style` are very unlikely to change, but minor
+style aspects and details might change according to the stability policy presented
+below. Ongoing style considerations are tracked on GitHub with the
+[style](https://github.com/psf/black/labels/T%3A%20style) issue label.
+
+(labels/stability-policy)=
+
+## Stability Policy
+
+The following policy applies for the _Black_ code style, in non pre-release versions of
+_Black_:
+
+- If code has been formatted with _Black_, it will remain unchanged when formatted with
+  the same options using any other release in the same calendar year.
+
+  This means projects can safely use `black ~= 22.0` without worrying about formatting
+  changes disrupting their project in 2022. We may still fix bugs where _Black_ crashes
+  on some code, and make other improvements that do not affect formatting.
+
+  In rare cases, we may make changes affecting code that has not been previously
+  formatted with _Black_. For example, we have had bugs where we accidentally removed
+  some comments. Such bugs can be fixed without breaking the stability policy.
+
+- The first release in a new calendar year _may_ contain formatting changes, although
+  these will be minimised as much as possible. This is to allow for improved formatting
+  enabled by newer Python language syntax as well as due to improvements in the
+  formatting logic.
+
+- The `--preview` flag is exempt from this policy. There are no guarantees around the
+  stability of the output with that flag passed into _Black_. This flag is intended for
+  allowing experimentation with the proposed changes to the _Black_ code style.
+
+Documentation for both the current and future styles can be found:
+
+- {doc}`current_style`
+- {doc}`future_style`
diff --git a/.vim/bundle/black/docs/the_black_code_style/index.rst b/.vim/bundle/black/docs/the_black_code_style/index.rst
deleted file mode 100644 (file)
index 4693437..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-The Black Code Style
-====================
-
-.. toctree::
-    :hidden:
-
-    Current style <current_style>
-    Future style <future_style>
-
-*Black* is a PEP 8 compliant opinionated formatter with its own style.
-
-It should be noted that while keeping the style unchanged throughout releases is a
-goal, the *Black* code style isn't set in stone. Sometimes it's modified in response to
-user feedback or even changes to the Python language!
-
-Documentation for both the current and future styles can be found:
-
-- :doc:`current_style`
-- :doc:`future_style`
index 75a4d925a544397b7f445c6b6439d9ab8310890a..f24fb34d91520c4f7e48f20ce7257fe9ab022b76 100644 (file)
@@ -4,10 +4,15 @@
 protocol. The main benefit of using it is to avoid the cost of starting up a new _Black_
 process every time you want to blacken a file.
 
+```{warning}
+`blackd` should not be run as a publicly accessible server as there are no security
+precautions in place to prevent abuse. **It is intended for local use only**.
+```
+
 ## Usage
 
 `blackd` is not packaged alongside _Black_ by default because it has additional
-dependencies. You will need to execute `pip install black[d]` to install it.
+dependencies. You will need to execute `pip install 'black[d]'` to install it.
 
 You can start the server on the default port, binding only to the local interface by
 running `blackd`. You will see a single line mentioning the server's version, and the
@@ -45,12 +50,18 @@ is rejected with `HTTP 501` (Not Implemented).
 The headers controlling how source code is formatted are:
 
 - `X-Line-Length`: corresponds to the `--line-length` command line flag.
+- `X-Skip-Source-First-Line`: corresponds to the `--skip-source-first-line` command line
+  flag. If present and its value is not an empty string, the first line of the source
+  code will be ignored.
 - `X-Skip-String-Normalization`: corresponds to the `--skip-string-normalization`
   command line flag. If present and its value is not the empty string, no string
   normalization will be performed.
 - `X-Skip-Magic-Trailing-Comma`: corresponds to the `--skip-magic-trailing-comma`
-  command line flag. If present and its value is not the empty string, trailing commas
+  command line flag. If present and its value is not an empty string, trailing commas
   will not be used as a reason to split lines.
+- `X-Preview`: corresponds to the `--preview` command line flag. If present and its
+  value is not an empty string, experimental and potentially disruptive style changes
+  will be used.
 - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the
   `--fast` command line flag.
 - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the
diff --git a/.vim/bundle/black/docs/usage_and_configuration/black_docker_image.md b/.vim/bundle/black/docs/usage_and_configuration/black_docker_image.md
new file mode 100644 (file)
index 0000000..85aec91
--- /dev/null
@@ -0,0 +1,51 @@
+# Black Docker image
+
+Official _Black_ Docker images are available on
+[Docker Hub](https://hub.docker.com/r/pyfound/black).
+
+_Black_ images with the following tags are available:
+
+- release numbers, e.g. `21.5b2`, `21.6b0`, `21.7b0` etc.\
+  ℹ Recommended for users who want to use a particular version of _Black_.
+- `latest_release` - tag created when a new version of _Black_ is released.\
+  ℹ Recommended for users who want to use released versions of _Black_. It maps to [the latest release](https://github.com/psf/black/releases/latest)
+  of _Black_.
+- `latest_prerelease` - tag created when a new alpha (prerelease) version of _Black_ is
+  released.\
+  ℹ Recommended for users who want to preview or test alpha versions of _Black_. Note that
+  the most recent release may be newer than any prerelease, because no prereleases are created
+  before most releases.
+- `latest` - tag used for the newest image of _Black_.\
+  ℹ Recommended for users who always want to use the latest version of _Black_, even before
+  it is released.
+
+There is one more tag used for _Black_ Docker images - `latest_non_release`. It is
+created for all unreleased
+[commits on the `main` branch](https://github.com/psf/black/commits/main). This tag is
+not meant to be used by external users.
+
+## Usage
+
+A permanent container doesn't have to be created to use _Black_ as a Docker image. It's
+enough to run _Black_ commands for the chosen image denoted as `:tag`. In the below
+examples, the `latest_release` tag is used. If `:tag` is omitted, the `latest` tag will
+be used.
+
+More about _Black_ usage can be found in
+[Usage and Configuration: The basics](./the_basics.md).
+
+### Check Black version
+
+```console
+$ docker run --rm pyfound/black:latest_release black --version
+```
+
+### Check code
+
+```console
+$ docker run --rm --volume $(pwd):/src --workdir /src pyfound/black:latest_release black --check .
+```
+
+_Remark_: besides [regular _Black_ exit codes](./the_basics.md) returned by `--check`
+option, [Docker exit codes](https://docs.docker.com/engine/reference/run/#exit-status)
+should also be considered.
index 1f436182dda415a92e84acb44f9299b19b6af58d..de1d5e6c11ef61c430db24429f8b51a69e887465 100644 (file)
@@ -22,10 +22,12 @@ run. The file is non-portable. The standard location on common operating systems
 `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
+To override the location of these files on all systems, set the environment variable
+`BLACK_CACHE_DIR` to the preferred location. Alternatively on macOS and Linux, set
 `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/<version>/`.
+in the directory you're running _Black_ from, set `BLACK_CACHE_DIR=.cache/black`.
+_Black_ will then write the above files to `.cache/black`. Note that `BLACK_CACHE_DIR`
+will take precedence over `XDG_CACHE_HOME` if both are set.
 
 ## .gitignore
 
diff --git a/.vim/bundle/black/docs/usage_and_configuration/index.md b/.vim/bundle/black/docs/usage_and_configuration/index.md
new file mode 100644 (file)
index 0000000..1c86a49
--- /dev/null
@@ -0,0 +1,28 @@
+# Usage and Configuration
+
+```{toctree}
+---
+hidden:
+---
+
+the_basics
+file_collection_and_discovery
+black_as_a_server
+black_docker_image
+```
+
+Sometimes, running _Black_ with its defaults and passing filepaths to it just won't cut
+it. Passing each file using paths will become burdensome, and maybe you would like
+_Black_ to not touch your files and just output diffs. And yes, you _can_ tweak certain
+parts of _Black_'s style, but please know that configurability in this area is
+purposefully limited.
+
+Using many of these more advanced features of _Black_ will require some configuration.
+Configuration that will either live on the command line or in a TOML configuration file.
+
+This section covers features of _Black_ and configuring _Black_ in detail:
+
+- {doc}`The basics <./the_basics>`
+- {doc}`File collection and discovery <file_collection_and_discovery>`
+- {doc}`Black as a server (blackd) <./black_as_a_server>`
+- {doc}`Black Docker image <./black_docker_image>`
diff --git a/.vim/bundle/black/docs/usage_and_configuration/index.rst b/.vim/bundle/black/docs/usage_and_configuration/index.rst
deleted file mode 100644 (file)
index 84a9c0c..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-Usage and Configuration
-=======================
-
-.. toctree::
-  :hidden:
-
-  the_basics
-  file_collection_and_discovery
-  black_as_a_server
-
-Sometimes, running *Black* with its defaults and passing filepaths to it just won't cut
-it. Passing each file using paths will become burdensome, and maybe you would like
-*Black* to not touch your files and just output diffs. And yes, you *can* tweak certain
-parts of *Black*'s style, but please know that configurability in this area is
-purposefully limited.
-
-Using many of these more advanced features of *Black* will require some configuration.
-Configuration that will either live on the command line or in a TOML configuration file.
-
-This section covers features of *Black* and configuring *Black* in detail:
-
-- :doc:`The basics <./the_basics>`
-- :doc:`File collection and discovery <file_collection_and_discovery>`
-- :doc:`Black as a server (blackd) <./black_as_a_server>`
index 49268b44f7c03da4d175269c81f2cc69617c3469..5b132a95eae5f35be0d9573ef83f4c1956050580 100644 (file)
@@ -4,11 +4,11 @@ Foundational knowledge on using and configuring Black.
 
 _Black_ is a well-behaved Unix-style command-line tool:
 
-- it does nothing if no sources are passed to it;
+- it does nothing if it finds no sources to format;
 - 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).
+- exits with code 0 unless an internal error occurred or a CLI option prompted it.
 
 ## Usage
 
@@ -26,57 +26,107 @@ python -m black {source_file_or_directory}
 
 ### Command line options
 
-_Black_ has quite a few knobs these days, although _Black_ is opinionated so style
-configuration options are deliberately limited and rarely added. You can list them by
-running `black --help`.
+The CLI options of _Black_ can be displayed by running `black --help`. All options are
+also covered in more detail below.
 
-<details>
+While _Black_ has quite a few knobs these days, it is still opinionated so style options
+are deliberately limited and rarely added.
+
+Note that all command-line options listed above can also be configured using a
+`pyproject.toml` file (more on that below).
 
-<summary>Help output</summary>
+#### `-c`, `--code`
 
-```{program-output} black --help
+Format the code passed in as a string.
 
+```console
+$ black --code "print ( 'hello, world' )"
+print("hello, world")
 ```
 
-</details>
+#### `-l`, `--line-length`
 
-### Code input alternatives
+How many characters per line to allow. The default is 88.
 
-#### Standard Input
+See also [the style documentation](labels/line-length).
 
-_Black_ supports formatting code via stdin, with the result being printed to stdout.
-Just let _Black_ know with `-` as the path.
+#### `-t`, `--target-version`
+
+Python versions that should be supported by Black's output. You can run `black --help`
+and look for the `--target-version` option to see the full list of supported versions.
+You should include all versions that your code supports. If you support Python 3.8
+through 3.11, you should write:
 
 ```console
-$ echo "print ( 'hello, world' )" | black -
-print("hello, world")
-reformatted -
-All done! ✨ 🍰 ✨
-1 file reformatted.
+$ black -t py38 -t py39 -t py310 -t py311
 ```
 
-**Tip:** if you need _Black_ to treat stdin input as a file passed directly via the CLI,
-use `--stdin-filename`. Useful to make sure _Black_ will respect the `--force-exclude`
-option on some editors that rely on using stdin.
+In a [configuration file](#configuration-via-a-file), you can write:
 
-#### As a string
+```toml
+target-version = ["py38", "py39", "py310", "py311"]
+```
 
-You can also pass code as a string using the `-c` / `--code` option.
+_Black_ uses this option to decide what grammar to use to parse your code. In addition,
+it may use it to decide what style to use. For example, support for a trailing comma
+after `*args` in a function call was added in Python 3.5, so _Black_ will add this comma
+only if the target versions are all Python 3.5 or higher:
 
 ```console
-$ black --code "print ( 'hello, world' )"
-print("hello, world")
+$ black --line-length=10 --target-version=py35 -c 'f(a, *args)'
+f(
+    a,
+    *args,
+)
+$ black --line-length=10 --target-version=py34 -c 'f(a, *args)'
+f(
+    a,
+    *args
+)
+$ black --line-length=10 --target-version=py34 --target-version=py35 -c 'f(a, *args)'
+f(
+    a,
+    *args
+)
 ```
 
-### Writeback and reporting
+#### `--pyi`
 
-By default _Black_ reformats the files given and/or found in place. Sometimes you need
-_Black_ to just tell you what it _would_ do without actually rewriting the Python files.
+Format all input files like typing stubs regardless of file extension. This is useful
+when piping source on standard input.
 
-There's two variations to this mode that are independently enabled by their respective
-flags. Both variations can be enabled at once.
+#### `--ipynb`
+
+Format all input files like Jupyter Notebooks regardless of file extension. This is
+useful when piping source on standard input.
+
+#### `--python-cell-magics`
+
+When processing Jupyter Notebooks, add the given magic to the list of known python-
+magics. Useful for formatting cells with custom python magics.
+
+#### `-S, --skip-string-normalization`
+
+By default, _Black_ uses double quotes for all strings and normalizes string prefixes,
+as described in [the style documentation](labels/strings). If this option is given,
+strings are left unchanged instead.
+
+#### `-C, --skip-magic-trailing-comma`
+
+By default, _Black_ uses existing trailing commas as an indication that short lines
+should be left separate, as described in
+[the style documentation](labels/magic-trailing-comma). If this option is given, the
+magic trailing comma is ignored.
+
+#### `--preview`
 
-#### Exit code
+Enable potentially disruptive style changes that may be added to Black's main
+functionality in the next major release. Read more about
+[our preview style](labels/preview-style).
+
+(labels/exit-code)=
+
+#### `--check`
 
 Passing `--check` will make _Black_ exit with:
 
@@ -106,17 +156,17 @@ $ echo $?
 123
 ```
 
-#### Diffs
+#### `--diff`
 
 Passing `--diff` will make _Black_ print out diffs that indicate what changes _Black_
 would've made. They are printed to stdout so capturing them is simple.
 
-If you'd like colored diffs, you can enable them with the `--color`.
+If you'd like colored diffs, you can enable them with `--color`.
 
 ```console
 $ black test.py --diff
---- test.py     2021-03-08 22:23:40.848954 +0000
-+++ test.py     2021-03-08 22:23:47.126319 +0000
+--- test.py     2021-03-08 22:23:40.848954+00:00
++++ test.py     2021-03-08 22:23:47.126319+00:00
 @@ -1 +1 @@
 -print ( 'hello, world' )
 +print("hello, world")
@@ -125,22 +175,93 @@ All done! ✨ 🍰 ✨
 1 file would be reformatted.
 ```
 
-### Output verbosity
+#### `--color` / `--no-color`
 
-_Black_ in general tries to produce the right amount of output, balancing between
-usefulness and conciseness. By default, _Black_ emits files modified and error messages,
-plus a short summary.
+Show (or do not show) colored diff. Only applies when `--diff` is given.
+
+#### `--fast` / `--safe`
+
+By default, _Black_ performs [an AST safety check](labels/ast-changes) after formatting
+your code. The `--fast` flag turns off this check and the `--safe` flag explicitly
+enables it.
+
+#### `--required-version`
+
+Require a specific version of _Black_ to be running. This is useful for ensuring that
+all contributors to your project are using the same version, because different versions
+of _Black_ may format code a little differently. This option can be set in a
+configuration file for consistent results across environments.
 
 ```console
-$ black src/
+$ black --version
+black, 23.10.0 (compiled: yes)
+$ black --required-version 23.10.0 -c "format = 'this'"
+format = "this"
+$ black --required-version 31.5b2 -c "still = 'beta?!'"
+Oh no! 💥 💔 💥 The required version does not match the running version!
+```
+
+You can also pass just the major version:
+
+```console
+$ black --required-version 22 -c "format = 'this'"
+format = "this"
+$ black --required-version 31 -c "still = 'beta?!'"
+Oh no! 💥 💔 💥 The required version does not match the running version!
+```
+
+Because of our [stability policy](../the_black_code_style/index.md), this will guarantee
+stable formatting, but still allow you to take advantage of improvements that do not
+affect formatting.
+
+#### `--include`
+
+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.
+
+#### `--exclude`
+
+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.
+
+#### `--extend-exclude`
+
+Like `--exclude`, but adds additional files and directories on top of the excluded ones.
+Useful if you simply want to add to the default.
+
+#### `--force-exclude`
+
+Like `--exclude`, but files and directories matching this regex will be excluded even
+when they are passed explicitly as arguments. This is useful when invoking _Black_
+programmatically on changed files, such as in a pre-commit hook or editor plugin.
+
+#### `--stdin-filename`
+
+The name of the file when passing it through stdin. Useful to make sure Black will
+respect the `--force-exclude` option on some editors that rely on using stdin.
+
+#### `-W`, `--workers`
+
+When _Black_ formats multiple files, it may use a process pool to speed up formatting.
+This option controls the number of parallel workers. This can also be specified via the
+`BLACK_NUM_WORKERS` environment variable.
+
+#### `-q`, `--quiet`
+
+Passing `-q` / `--quiet` will cause _Black_ to stop emitting all non-critical output.
+Error messages will still be emitted (which can silenced by `2>/dev/null`).
+
+```console
+$ black src/ -q
 error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio
-reformatted src/black_primer/lib.py
-reformatted src/blackd/__init__.py
-reformatted src/black/__init__.py
-Oh no! 💥 💔 💥
-3 files reformatted, 2 files left unchanged, 1 file failed to reformat.
 ```
 
+#### `-v`, `--verbose`
+
 Passing `-v` / `--verbose` will cause _Black_ to also emit messages about files that
 were not changed or were ignored due to exclusion patterns. If _Black_ is using a
 configuration file, a blue message detailing which one it is using will be emitted.
@@ -159,35 +280,86 @@ Oh no! 💥 💔 💥
 3 files reformatted, 2 files left unchanged, 1 file failed to reformat
 ```
 
-Passing `-q` / `--quiet` will cause _Black_ to stop emitting all non-critial output.
-Error messages will still be emitted (which can silenced by `2>/dev/null`).
+#### `--version`
+
+You can check the version of _Black_ you have installed using the `--version` flag.
 
 ```console
-$ black src/ -q
-error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio
+$ black --version
+black, 23.10.0
 ```
 
-### Versions
+#### `--config`
 
-You can check the version of _Black_ you have installed using the `--version` flag.
+Read configuration options from a configuration file. See
+[below](#configuration-via-a-file) for more details on the configuration file.
+
+#### `-h`, `--help`
+
+Show available command-line options and exit.
+
+### Environment variable options
+
+_Black_ supports the following configuration via environment variables.
+
+#### `BLACK_CACHE_DIR`
+
+The directory where _Black_ should store its cache.
+
+#### `BLACK_NUM_WORKERS`
+
+The number of parallel workers _Black_ should use. The command line option `-W` /
+`--workers` takes precedence over this environment variable.
+
+### Code input alternatives
+
+_Black_ supports formatting code via stdin, with the result being printed to stdout.
+Just let _Black_ know with `-` as the path.
 
 ```console
-$ black --version
-black, version 21.9b0
+$ echo "print ( 'hello, world' )" | black -
+print("hello, world")
+reformatted -
+All done! ✨ 🍰 ✨
+1 file reformatted.
 ```
 
-An option to require a specific version to be running is also provided.
+**Tip:** if you need _Black_ to treat stdin input as a file passed directly via the CLI,
+use `--stdin-filename`. Useful to make sure _Black_ will respect the `--force-exclude`
+option on some editors that rely on using stdin.
+
+You can also pass code as a string using the `-c` / `--code` option.
+
+### Writeback and reporting
+
+By default _Black_ reformats the files given and/or found in place. Sometimes you need
+_Black_ to just tell you what it _would_ do without actually rewriting the Python files.
+
+There's two variations to this mode that are independently enabled by their respective
+flags:
+
+- `--check` (exit with code 1 if any file would be reformatted)
+- `--diff` (print a diff instead of reformatting files)
+
+Both variations can be enabled at once.
+
+### Output verbosity
+
+_Black_ in general tries to produce the right amount of output, balancing between
+usefulness and conciseness. By default, _Black_ emits files modified and error messages,
+plus a short summary.
 
 ```console
-$ black --required-version 21.9b0 -c "format = 'this'"
-format = "this"
-$ black --required-version 31.5b2 -c "still = 'beta?!'"
-Oh no! 💥 💔 💥 The required version does not match the running version!
+$ black src/
+error: cannot format src/black_primer/cli.py: Cannot parse: 5:6: mport asyncio
+reformatted src/black_primer/lib.py
+reformatted src/blackd/__init__.py
+reformatted src/black/__init__.py
+Oh no! 💥 💔 💥
+3 files reformatted, 2 files left unchanged, 1 file failed to reformat.
 ```
 
-This is useful for example when running _Black_ in multiple environments that haven't
-necessarily installed the correct version. This option can be set in a configuration
-file for consistent results across environments.
+The `--quiet` and `--verbose` flags control output verbosity.
 
 ## Configuration via a file
 
@@ -204,9 +376,10 @@ code in compliance with many other _Black_ formatted projects.
 
 [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.
+of tools like [Poetry](https://python-poetry.org/),
+[Flit](https://flit.readthedocs.io/en/latest/), or
+[Hatch](https://hatch.pypa.io/latest/) it can fully replace the need for `setup.py` and
+`setup.cfg` files.
 
 ### Where _Black_ looks for the file
 
@@ -259,10 +432,14 @@ expressions by Black. Use `[ ]` to denote a significant space character.
 line-length = 88
 target-version = ['py37']
 include = '\.pyi?$'
+# 'extend-exclude' excludes files or directories in addition to the defaults
 extend-exclude = '''
 # A regex preceded with ^/ will apply only to files and directories
 # in the root of the project.
-^/foo.py  # exclude a file named foo.py in the root of the project (in addition to the defaults)
+(
+  ^/foo.py    # exclude a file named foo.py in the root of the project
+  | .*_pb2.py  # exclude autogenerated Protocol Buffer files anywhere in the project
+)
 '''
 ```
 
@@ -280,9 +457,6 @@ file hierarchy.
 
 ## Next steps
 
-You've probably noted that not all of the options you can pass to _Black_ have been
-covered. Don't worry, the rest will be covered in a later section.
-
 A good next step would be configuring auto-discovery so `black .` is all you need
 instead of laborously listing every file or directory. You can get started by heading
 over to [File collection and discovery](./file_collection_and_discovery.md).
index 3df05c1a722b637cb7b9c8abedbbcc907fc3a217..ba5d6f65fbe5c9c0034b1569d8897f28713780f0 100755 (executable)
@@ -10,26 +10,16 @@ from argparse import ArgumentParser, Namespace
 from concurrent.futures import ThreadPoolExecutor
 from functools import lru_cache, partial
 from pathlib import Path
-from typing import (  # type: ignore # typing can't see Literal
-    Generator,
-    List,
-    Literal,
-    NamedTuple,
-    Optional,
-    Tuple,
-    Union,
-    cast,
-)
+from typing import Generator, List, NamedTuple, Optional, Tuple, Union, cast
 from urllib.request import urlopen, urlretrieve
 
 PYPI_INSTANCE = "https://pypi.org/pypi"
 PYPI_TOP_PACKAGES = (
-    "https://hugovk.github.io/top-pypi-packages/top-pypi-packages-{days}-days.json"
+    "https://hugovk.github.io/top-pypi-packages/top-pypi-packages-30-days.min.json"
 )
 INTERNAL_BLACK_REPO = f"{tempfile.gettempdir()}/__black"
 
 ArchiveKind = Union[tarfile.TarFile, zipfile.ZipFile]
-Days = Union[Literal[30], Literal[365]]
 
 subprocess.run = partial(subprocess.run, check=True)  # type: ignore
 # https://github.com/python/mypy/issues/1484
@@ -64,8 +54,8 @@ def get_pypi_download_url(package: str, version: Optional[str]) -> str:
     return cast(str, source["url"])
 
 
-def get_top_packages(days: Days) -> List[str]:
-    with urlopen(PYPI_TOP_PACKAGES.format(days=days)) as page:
+def get_top_packages() -> List[str]:
+    with urlopen(PYPI_TOP_PACKAGES) as page:
         result = json.load(page)
 
     return [package["project"] for package in result["rows"]]
@@ -128,13 +118,12 @@ DEFAULT_SLICE = slice(None)  # for flake8
 
 def download_and_extract_top_packages(
     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)
-        for package in executor.map(bound_downloader, get_top_packages(days)[limit]):
+        for package in executor.map(bound_downloader, get_top_packages()[limit]):
             if package is not None:
                 yield package
 
@@ -254,11 +243,9 @@ def format_repos(repos: Tuple[Path, ...], options: Namespace) -> None:
 
 
 def main() -> None:
-    parser = ArgumentParser(
-        description="""Black Gallery is a script that
+    parser = ArgumentParser(description="""Black Gallery is a script that
     automates the process of applying different Black versions to a selected
-    PyPI package and seeing the results between versions."""
-    )
+    PyPI package and seeing the results between versions.""")
 
     group = parser.add_mutually_exclusive_group(required=True)
     group.add_argument("-p", "--pypi-package", help="PyPI package to download.")
diff --git a/.vim/bundle/black/mypy.ini b/.vim/bundle/black/mypy.ini
deleted file mode 100644 (file)
index 7e563e6..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-[mypy]
-# Specify the target platform details in config, so your developers are
-# free to run mypy on Windows, Linux, or macOS and get consistent
-# results.
-python_version=3.6
-platform=linux
-
-mypy_path=src
-
-show_column_numbers=True
-
-# show error messages from unrelated files
-follow_imports=normal
-
-# suppress errors about unsatisfied imports
-ignore_missing_imports=True
-
-# be strict
-disallow_untyped_calls=True
-warn_return_any=True
-strict_optional=True
-warn_no_return=True
-warn_redundant_casts=True
-warn_unused_ignores=True
-# Until we're not supporting 3.6 primer needs this
-disallow_any_generics=False
-
-# The following are off by default.  Flip them on if you feel
-# adventurous.
-disallow_untyped_defs=True
-check_untyped_defs=True
-
-# No incremental mode
-cache_dir=/dev/null
-
-[mypy-aiohttp.*]
-follow_imports=skip
-[mypy-black]
-# The following is because of `patch_click()`. Remove when
-# we drop Python 3.6 support.
-warn_unused_ignores=False
index 90d2047790b2ba2e916b4cae3e18518117334b01..543184e1cd4f373d3239cfdaf429c351e033b5eb 100644 (file)
 "  1.2:
 "    - use autoload script
 
+if exists("g:load_black")
+  finish
+endif
+
 if v:version < 700 || !has('python3')
     func! __BLACK_MISSING()
         echo "The black.vim plugin requires vim7.0+ with Python 3.6 support."
@@ -25,10 +29,6 @@ if v:version < 700 || !has('python3')
     finish
 endif
 
-if exists("g:load_black")
-  finish
-endif
-
 let g:load_black = "py1.0"
 if !exists("g:black_virtualenv")
   if has("nvim")
@@ -50,12 +50,25 @@ if !exists("g:black_skip_string_normalization")
     let g:black_skip_string_normalization = 0
   endif
 endif
+if !exists("g:black_skip_magic_trailing_comma")
+  if exists("g:black_magic_trailing_comma")
+    let g:black_skip_magic_trailing_comma = !g:black_magic_trailing_comma
+  else
+    let g:black_skip_magic_trailing_comma = 0
+  endif
+endif
 if !exists("g:black_quiet")
   let g:black_quiet = 0
 endif
 if !exists("g:black_target_version")
   let g:black_target_version = ""
 endif
+if !exists("g:black_use_virtualenv")
+  let g:black_use_virtualenv = 1
+endif
+if !exists("g:black_preview")
+  let g:black_preview = 0
+endif
 
 function BlackComplete(ArgLead, CmdLine, CursorPos)
   return [
@@ -64,6 +77,7 @@ function BlackComplete(ArgLead, CmdLine, CursorPos)
 \    'target_version=py37',
 \    'target_version=py38',
 \    'target_version=py39',
+\    'target_version=py310',
 \  ]
 endfunction
 
index 73e19608108d5541a4d1e49081b1ef2e9207b829..8c55076e4c9331048f196affa4f40957d547663a 100644 (file)
 
 [tool.black]
 line-length = 88
-target-version = ['py36', 'py37', 'py38']
+target-version = ['py37', 'py38']
 include = '\.pyi?$'
 extend-exclude = '''
 /(
   # The following are specific to Black, you probably don't want those.
-  | blib2to3
-  | tests/data
+  tests/data
   | profiling
 )/
 '''
+# We use preview style for formatting Black itself. If you
+# want stable formatting across releases, you should keep
+# this off.
+preview = true
 
-
-# Build system information below.
+# Build system information and other project-specific configuration below.
 # NOTE: You don't need this in your own Black configuration.
 
 [build-system]
-requires = ["setuptools>=45.0", "setuptools_scm[toml]>=6.3.1", "wheel"]
-build-backend = "setuptools.build_meta"
+requires = ["hatchling>=1.8.0", "hatch-vcs", "hatch-fancy-pypi-readme"]
+build-backend = "hatchling.build"
+
+[project]
+name = "black"
+description = "The uncompromising code formatter."
+license = { text = "MIT" }
+requires-python = ">=3.8"
+authors = [
+  { name = "Łukasz Langa", email = "lukasz@langa.pl" },
+]
+keywords = [
+  "automation",
+  "autopep8",
+  "formatter",
+  "gofmt",
+  "pyfmt",
+  "rustfmt",
+  "yapf",
+]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Environment :: Console",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: MIT License",
+  "Operating System :: OS Independent",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Programming Language :: Python :: 3.12",
+  "Topic :: Software Development :: Libraries :: Python Modules",
+  "Topic :: Software Development :: Quality Assurance",
+]
+dependencies = [
+  "click>=8.0.0",
+  "mypy_extensions>=0.4.3",
+  "packaging>=22.0",
+  "pathspec>=0.9.0",
+  "platformdirs>=2",
+  "tomli>=1.1.0; python_version < '3.11'",
+  "typing_extensions>=4.0.1; python_version < '3.11'",
+]
+dynamic = ["readme", "version"]
+
+[project.optional-dependencies]
+colorama = ["colorama>=0.4.3"]
+uvloop = ["uvloop>=0.15.2"]
+d = [
+  "aiohttp>=3.7.4",
+]
+jupyter = [
+  "ipython>=7.8.0",
+  "tokenize-rt>=3.2.0",
+]
+
+[project.scripts]
+black = "black:patched_main"
+blackd = "blackd:patched_main [d]"
+
+[project.urls]
+Changelog = "https://github.com/psf/black/blob/main/CHANGES.md"
+Homepage = "https://github.com/psf/black"
+
+[tool.hatch.metadata.hooks.fancy-pypi-readme]
+content-type = "text/markdown"
+fragments = [
+  { path = "README.md" },
+  { path = "CHANGES.md" },
+]
+
+[tool.hatch.version]
+source = "vcs"
+
+[tool.hatch.build.hooks.vcs]
+version-file = "src/_black_version.py"
+template = '''
+version = "{version}"
+'''
+
+[tool.hatch.build.targets.sdist]
+exclude = ["/profiling"]
+
+[tool.hatch.build.targets.wheel]
+only-include = ["src"]
+sources = ["src"]
+
+[tool.hatch.build.targets.wheel.hooks.mypyc]
+enable-by-default = false
+dependencies = [
+  "hatch-mypyc>=0.16.0",
+  "mypy==1.5.1",
+  "click==8.1.3",  # avoid https://github.com/pallets/click/issues/2558
+]
+require-runtime-dependencies = true
+exclude = [
+  # There's no good reason for blackd to be compiled.
+  "/src/blackd",
+  # Not performance sensitive, so save bytes + compilation time:
+  "/src/blib2to3/__init__.py",
+  "/src/blib2to3/pgen2/__init__.py",
+  "/src/black/output.py",
+  "/src/black/concurrency.py",
+  "/src/black/files.py",
+  "/src/black/report.py",
+  # Breaks the test suite when compiled (and is also useless):
+  "/src/black/debug.py",
+  # Compiled modules can't be run directly and that's a problem here:
+  "/src/black/__main__.py",
+]
+mypy-args = ["--ignore-missing-imports"]
+options = { debug_level = "0" }
+
+[tool.cibuildwheel]
+build-verbosity = 1
+# So these are the environments we target:
+# - Python: CPython 3.8+ only
+# - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64
+# - OS: Linux (no musl), Windows, and macOS
+build = "cp3*"
+skip = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "pp*", "cp312-*"]
+# This is the bare minimum needed to run the test suite. Pulling in the full
+# test_requirements.txt would download a bunch of other packages not necessary
+# here and would slow down the testing step a fair bit.
+test-requires = ["pytest>=6.1.1"]
+test-command = 'pytest {project} -k "not incompatible_with_mypyc"'
+test-extras = ["d"," jupyter"]
+# Skip trying to test arm64 builds on Intel Macs. (so cross-compilation doesn't
+# straight up crash)
+test-skip = ["*-macosx_arm64", "*-macosx_universal2:arm64"]
+
+[tool.cibuildwheel.environment]
+HATCH_BUILD_HOOKS_ENABLE = "1"
+MYPYC_OPT_LEVEL = "3"
+MYPYC_DEBUG_LEVEL = "0"
+# CPython 3.11 wheels aren't available for aiohttp and building a Cython extension
+# from source also doesn't work.
+AIOHTTP_NO_EXTENSIONS = "1"
+
+[tool.cibuildwheel.linux]
+before-build = [
+    "yum install -y clang gcc",
+]
+
+[tool.cibuildwheel.linux.environment]
+HATCH_BUILD_HOOKS_ENABLE = "1"
+MYPYC_OPT_LEVEL = "3"
+MYPYC_DEBUG_LEVEL = "0"
+# Black needs Clang to compile successfully on Linux.
+CC = "clang"
+AIOHTTP_NO_EXTENSIONS = "1"
+
+[tool.isort]
+atomic = true
+profile = "black"
+line_length = 88
+skip_gitignore = true
+skip_glob = ["tests/data", "profiling"]
+known_first_party = ["black", "blib2to3", "blackd", "_black_version"]
 
 [tool.pytest.ini_options]
 # Option below requires `tests/optional.py`
+addopts = "--strict-config --strict-markers"
 optional-tests = [
-  "no_python2: run when `python2` extra NOT installed",
   "no_blackd: run when `d` extra NOT installed",
   "no_jupyter: run when `jupyter` extra NOT installed",
 ]
+markers = [
+  "incompatible_with_mypyc: run when testing mypyc compiled black"
+]
+xfail_strict = true
+filterwarnings = [
+    "error",
+    # this is mitigated by a try/catch in https://github.com/psf/black/pull/2974/
+    # this ignore can be removed when support for aiohttp 3.7 is dropped.
+    '''ignore:Decorator `@unittest_run_loop` is no longer needed in aiohttp 3\.8\+:DeprecationWarning''',
+    # this is mitigated by a try/catch in https://github.com/psf/black/pull/3198/
+    # this ignore can be removed when support for aiohttp 3.x is dropped.
+    '''ignore:Middleware decorator is deprecated since 4\.0 and its behaviour is default, you can simply remove this decorator:DeprecationWarning''',
+    # aiohttp is using deprecated cgi modules - Safe to remove when fixed:
+    # https://github.com/aio-libs/aiohttp/issues/6905
+    '''ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning''',
+    # Work around https://github.com/pytest-dev/pytest/issues/10977 for Python 3.12
+    '''ignore:(Attribute s|Attribute n|ast.Str|ast.Bytes|ast.NameConstant|ast.Num) is deprecated and will be removed in Python 3.14:DeprecationWarning''',
+    # Will be fixed with aiohttp 3.9.0
+    # https://github.com/aio-libs/aiohttp/pull/7302
+    "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning",
+]
+[tool.coverage.report]
+omit = [
+  "src/blib2to3/*",
+  "tests/data/*",
+  "*/site-packages/*",
+  ".tox/*"
+]
+[tool.coverage.run]
+relative_files = true
+
+[tool.mypy]
+# Specify the target platform details in config, so your developers are
+# free to run mypy on Windows, Linux, or macOS and get consistent
+# results.
+python_version = "3.8"
+mypy_path = "src"
+strict = true
+# Unreachable blocks have been an issue when compiling mypyc, let's try to avoid 'em in the first place.
+warn_unreachable = true
+implicit_reexport = true
+show_error_codes = true
+show_column_numbers = true
+
+[[tool.mypy.overrides]]
+module = ["pathspec.*", "IPython.*", "colorama.*", "tokenize_rt.*", "uvloop.*", "_black_version.*"]
+ignore_missing_imports = true
+
+# CI only checks src/, but in case users are running LSP or similar we explicitly ignore
+# errors in test data files.
+[[tool.mypy.overrides]]
+module = ["tests.data.*"]
+ignore_errors = true
index 9560b3b840100b5de0507fc17f872c010bfc0666..107c6444dca348d368dc8ab35895fa14600e7490 100644 (file)
@@ -14,7 +14,7 @@ import sys
 
 import commonmark
 import yaml
-from bs4 import BeautifulSoup
+from bs4 import BeautifulSoup  # type: ignore[import]
 
 
 def main(changes: str, source_version_control: str) -> None:
index c62780d97ab08ec3f3fbe0ed553a449af53ba2db..0f42bafe334e5198235530c193c09fa7d75d8302 100644 (file)
@@ -8,7 +8,7 @@ import os
 import sys
 
 import commonmark
-from bs4 import BeautifulSoup
+from bs4 import BeautifulSoup  # type: ignore[import]
 
 
 def main(changes: str, the_basics: str) -> None:
@@ -20,20 +20,21 @@ def main(changes: str, the_basics: str) -> None:
 
     the_basics_html = commonmark.commonmark(the_basics)
     the_basics_soup = BeautifulSoup(the_basics_html, "html.parser")
-    (version_example,) = [
+    version_examples = [
         code_block.string
         for code_block in the_basics_soup.find_all(class_="language-console")
         if "$ black --version" in code_block.string
     ]
 
     for tag in tags:
-        if tag in version_example and tag != latest_tag:
-            print(
-                "Please set the version in the ``black --version`` "
-                "example from ``the_basics.md`` to be the latest one.\n"
-                f"Expected {latest_tag}, got {tag}.\n"
-            )
-            sys.exit(1)
+        for version_example in version_examples:
+            if tag in version_example and tag != latest_tag:
+                print(
+                    "Please set the version in the ``black --version`` "
+                    "examples from ``the_basics.md`` to be the latest one.\n"
+                    f"Expected {latest_tag}, got {tag}.\n"
+                )
+                sys.exit(1)
 
 
 if __name__ == "__main__":
diff --git a/.vim/bundle/black/scripts/diff_shades_gha_helper.py b/.vim/bundle/black/scripts/diff_shades_gha_helper.py
new file mode 100644 (file)
index 0000000..895516d
--- /dev/null
@@ -0,0 +1,236 @@
+"""Helper script for psf/black's diff-shades Github Actions integration.
+
+diff-shades is a tool for analyzing what happens when you run Black on
+OSS code capturing it for comparisons or other usage. It's used here to
+help measure the impact of a change *before* landing it (in particular
+posting a comment on completion for PRs).
+
+This script exists as a more maintainable alternative to using inline
+Javascript in the workflow YAML files. The revision configuration and
+resolving, caching, and PR comment logic is contained here.
+
+For more information, please see the developer docs:
+
+https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades
+"""
+
+import json
+import os
+import platform
+import pprint
+import subprocess
+import sys
+import zipfile
+from base64 import b64encode
+from io import BytesIO
+from pathlib import Path
+from typing import Any
+
+import click
+import urllib3
+from packaging.version import Version
+
+if sys.version_info >= (3, 8):
+    from typing import Final, Literal
+else:
+    from typing_extensions import Final, Literal
+
+COMMENT_FILE: Final = ".pr-comment.json"
+DIFF_STEP_NAME: Final = "Generate HTML diff report"
+DOCS_URL: Final = (
+    "https://black.readthedocs.io/en/latest/"
+    "contributing/gauging_changes.html#diff-shades"
+)
+USER_AGENT: Final = f"psf/black diff-shades workflow via urllib3/{urllib3.__version__}"
+SHA_LENGTH: Final = 10
+GH_API_TOKEN: Final = os.getenv("GITHUB_TOKEN")
+REPO: Final = os.getenv("GITHUB_REPOSITORY", default="psf/black")
+http = urllib3.PoolManager()
+
+
+def set_output(name: str, value: str) -> None:
+    if len(value) < 200:
+        print(f"[INFO]: setting '{name}' to '{value}'")
+    else:
+        print(f"[INFO]: setting '{name}' to [{len(value)} chars]")
+
+    if "GITHUB_OUTPUT" in os.environ:
+        if "\n" in value:
+            # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
+            delimiter = b64encode(os.urandom(16)).decode()
+            value = f"{delimiter}\n{value}\n{delimiter}"
+            command = f"{name}<<{value}"
+        else:
+            command = f"{name}={value}"
+        with open(os.environ["GITHUB_OUTPUT"], "a") as f:
+            print(command, file=f)
+
+
+def http_get(url: str, *, is_json: bool = True, **kwargs: Any) -> Any:
+    headers = kwargs.get("headers") or {}
+    headers["User-Agent"] = USER_AGENT
+    if "github" in url:
+        if GH_API_TOKEN:
+            headers["Authorization"] = f"token {GH_API_TOKEN}"
+        headers["Accept"] = "application/vnd.github.v3+json"
+    kwargs["headers"] = headers
+
+    r = http.request("GET", url, **kwargs)
+    if is_json:
+        data = json.loads(r.data.decode("utf-8"))
+    else:
+        data = r.data
+    print(f"[INFO]: issued GET request for {r.geturl()}")
+    if not (200 <= r.status < 300):
+        pprint.pprint(dict(r.info()))
+        pprint.pprint(data)
+        raise RuntimeError(f"unexpected status code: {r.status}")
+
+    return data
+
+
+def get_main_revision() -> str:
+    data = http_get(
+        f"https://api.github.com/repos/{REPO}/commits",
+        fields={"per_page": "1", "sha": "main"},
+    )
+    assert isinstance(data[0]["sha"], str)
+    return data[0]["sha"]
+
+
+def get_pr_revision(pr: int) -> str:
+    data = http_get(f"https://api.github.com/repos/{REPO}/pulls/{pr}")
+    assert isinstance(data["head"]["sha"], str)
+    return data["head"]["sha"]
+
+
+def get_pypi_version() -> Version:
+    data = http_get("https://pypi.org/pypi/black/json")
+    versions = [Version(v) for v in data["releases"]]
+    sorted_versions = sorted(versions, reverse=True)
+    return sorted_versions[0]
+
+
+@click.group()
+def main() -> None:
+    pass
+
+
+@main.command("config", help="Acquire run configuration and metadata.")
+@click.argument("event", type=click.Choice(["push", "pull_request"]))
+def config(event: Literal["push", "pull_request"]) -> None:
+    import diff_shades  # type: ignore[import]
+
+    if event == "push":
+        jobs = [{"mode": "preview-changes", "force-flag": "--force-preview-style"}]
+        # Push on main, let's use PyPI Black as the baseline.
+        baseline_name = str(get_pypi_version())
+        baseline_cmd = f"git checkout {baseline_name}"
+        target_rev = os.getenv("GITHUB_SHA")
+        assert target_rev is not None
+        target_name = "main-" + target_rev[:SHA_LENGTH]
+        target_cmd = f"git checkout {target_rev}"
+
+    elif event == "pull_request":
+        jobs = [
+            {"mode": "preview-changes", "force-flag": "--force-preview-style"},
+            {"mode": "assert-no-changes", "force-flag": "--force-stable-style"},
+        ]
+        # PR, let's use main as the baseline.
+        baseline_rev = get_main_revision()
+        baseline_name = "main-" + baseline_rev[:SHA_LENGTH]
+        baseline_cmd = f"git checkout {baseline_rev}"
+        pr_ref = os.getenv("GITHUB_REF")
+        assert pr_ref is not None
+        pr_num = int(pr_ref[10:-6])
+        pr_rev = get_pr_revision(pr_num)
+        target_name = f"pr-{pr_num}-{pr_rev[:SHA_LENGTH]}"
+        target_cmd = f"gh pr checkout {pr_num} && git merge origin/main"
+
+    env = f"{platform.system()}-{platform.python_version()}-{diff_shades.__version__}"
+    for entry in jobs:
+        entry["baseline-analysis"] = f"{entry['mode']}-{baseline_name}.json"
+        entry["baseline-setup-cmd"] = baseline_cmd
+        entry["target-analysis"] = f"{entry['mode']}-{target_name}.json"
+        entry["target-setup-cmd"] = target_cmd
+        entry["baseline-cache-key"] = f"{env}-{baseline_name}-{entry['mode']}"
+        if event == "pull_request":
+            # These are only needed for the PR comment.
+            entry["baseline-sha"] = baseline_rev
+            entry["target-sha"] = pr_rev
+
+    set_output("matrix", json.dumps(jobs, indent=None))
+    pprint.pprint(jobs)
+
+
+@main.command("comment-body", help="Generate the body for a summary PR comment.")
+@click.argument("baseline", type=click.Path(exists=True, path_type=Path))
+@click.argument("target", type=click.Path(exists=True, path_type=Path))
+@click.argument("baseline-sha")
+@click.argument("target-sha")
+@click.argument("pr-num", type=int)
+def comment_body(
+    baseline: Path, target: Path, baseline_sha: str, target_sha: str, pr_num: int
+) -> None:
+    # fmt: off
+    cmd = [
+        sys.executable, "-m", "diff_shades", "--no-color",
+        "compare", str(baseline), str(target), "--quiet", "--check"
+    ]
+    # fmt: on
+    proc = subprocess.run(cmd, stdout=subprocess.PIPE, encoding="utf-8")
+    if not proc.returncode:
+        body = (
+            f"**diff-shades** reports zero changes comparing this PR ({target_sha}) to"
+            f" main ({baseline_sha}).\n\n---\n\n"
+        )
+    else:
+        body = (
+            f"**diff-shades** results comparing this PR ({target_sha}) to main"
+            f" ({baseline_sha}). The full diff is [available in the logs]"
+            f'($job-diff-url) under the "{DIFF_STEP_NAME}" step.'
+        )
+        body += "\n```text\n" + proc.stdout.strip() + "\n```\n"
+    body += (
+        f"[**What is this?**]({DOCS_URL}) | [Workflow run]($workflow-run-url) |"
+        " [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)"
+    )
+    print(f"[INFO]: writing comment details to {COMMENT_FILE}")
+    with open(COMMENT_FILE, "w", encoding="utf-8") as f:
+        json.dump({"body": body, "pr-number": pr_num}, f)
+
+
+@main.command("comment-details", help="Get PR comment resources from a workflow run.")
+@click.argument("run-id")
+def comment_details(run_id: str) -> None:
+    data = http_get(f"https://api.github.com/repos/{REPO}/actions/runs/{run_id}")
+    if data["event"] != "pull_request" or data["conclusion"] == "cancelled":
+        set_output("needs-comment", "false")
+        return
+
+    set_output("needs-comment", "true")
+    jobs = http_get(data["jobs_url"])["jobs"]
+    job = next(j for j in jobs if j["name"] == "analysis / preview-changes")
+    diff_step = next(s for s in job["steps"] if s["name"] == DIFF_STEP_NAME)
+    diff_url = job["html_url"] + f"#step:{diff_step['number']}:1"
+
+    artifacts = http_get(data["artifacts_url"])["artifacts"]
+    comment_artifact = next(a for a in artifacts if a["name"] == COMMENT_FILE)
+    comment_url = comment_artifact["archive_download_url"]
+    comment_zip = BytesIO(http_get(comment_url, is_json=False))
+    with zipfile.ZipFile(comment_zip) as zfile:
+        with zfile.open(COMMENT_FILE) as rf:
+            comment_data = json.loads(rf.read().decode("utf-8"))
+
+    set_output("pr-number", str(comment_data["pr-number"]))
+    body = comment_data["body"]
+    # It's more convenient to fill in these fields after the first workflow is done
+    # since this command can access the workflows API (doing it in the main workflow
+    # while it's still in progress seems impossible).
+    body = body.replace("$workflow-run-url", data["html_url"])
+    body = body.replace("$job-diff-url", diff_url)
+    set_output("comment-body", body)
+
+
+if __name__ == "__main__":
+    main()
similarity index 86%
rename from .vim/bundle/black/fuzz.py
rename to .vim/bundle/black/scripts/fuzz.py
index a9ca8eff8b09b64bb70420add85dd0e890b4b60f..929d3eac4f57132dfb5c3e5364109a30052cbfa4 100644 (file)
@@ -8,7 +8,8 @@ a coverage-guided fuzzer I'm working on.
 import re
 
 import hypothesmith
-from hypothesis import HealthCheck, given, settings, strategies as st
+from hypothesis import HealthCheck, given, settings
+from hypothesis import strategies as st
 
 import black
 from blib2to3.pgen2.tokenize import TokenError
@@ -20,7 +21,7 @@ from blib2to3.pgen2.tokenize import TokenError
     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.
+    suppress_health_check=list(HealthCheck),  # this is slow and filter-heavy.
 )
 @given(
     # Note that while Hypothesmith might generate code unlike that written by
@@ -32,7 +33,9 @@ from blib2to3.pgen2.tokenize import TokenError
         black.FileMode,
         line_length=st.just(88) | st.integers(0, 200),
         string_normalization=st.booleans(),
+        preview=st.booleans(),
         is_pyi=st.booleans(),
+        magic_trailing_comma=st.booleans(),
     ),
 )
 def test_idempotent_any_syntatically_valid_python(
@@ -46,7 +49,7 @@ def test_idempotent_any_syntatically_valid_python(
         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 cope with it.  See issues #970, #1012, #1358, and #1557.
+        # able to cope with it.  See issues #970, #1012
         # TODO: remove this try-except block when issues are resolved.
         return
     except TokenError as e:
@@ -76,10 +79,14 @@ if __name__ == "__main__":
     # (if you want only bounded fuzzing, just use `pytest fuzz.py`)
     try:
         import sys
-        import atheris
+
+        import atheris  # type: ignore[import]
     except ImportError:
         pass
     else:
         test = test_idempotent_any_syntatically_valid_python
-        atheris.Setup(sys.argv, test.hypothesis.fuzz_one_input)
+        atheris.Setup(
+            sys.argv,
+            test.hypothesis.fuzz_one_input,  # type: ignore[attr-defined]
+        )
         atheris.Fuzz()
diff --git a/.vim/bundle/black/scripts/make_width_table.py b/.vim/bundle/black/scripts/make_width_table.py
new file mode 100644 (file)
index 0000000..3c7cae6
--- /dev/null
@@ -0,0 +1,66 @@
+"""Generates a width table for Unicode characters.
+
+This script generates a width table for Unicode characters that are not
+narrow (width 1). The table is written to src/black/_width_table.py (note
+that although this file is generated, it is checked into Git) and is used
+by the char_width() function in src/black/strings.py.
+
+You should run this script when you upgrade wcwidth, which is expected to
+happen when a new Unicode version is released. The generated table contains
+the version of wcwidth and Unicode that it was generated for.
+
+In order to run this script, you need to install the latest version of wcwidth.
+You can do this by running:
+
+    pip install -U wcwidth
+
+"""
+
+import sys
+from os.path import basename, dirname, join
+from typing import Iterable, Tuple
+
+import wcwidth  # type: ignore[import]
+
+
+def make_width_table() -> Iterable[Tuple[int, int, int]]:
+    start_codepoint = -1
+    end_codepoint = -1
+    range_width = -2
+    for codepoint in range(0, sys.maxunicode + 1):
+        width = wcwidth.wcwidth(chr(codepoint))
+        if width <= 1:
+            # Ignore narrow characters along with zero-width characters so that
+            # they are treated as single-width.  Note that treating zero-width
+            # characters as single-width is consistent with the heuristics built
+            # on top of str.isascii() in the str_width() function in strings.py.
+            continue
+        if start_codepoint < 0:
+            start_codepoint = codepoint
+            range_width = width
+        elif width != range_width or codepoint != end_codepoint + 1:
+            yield (start_codepoint, end_codepoint, range_width)
+            start_codepoint = codepoint
+            range_width = width
+        end_codepoint = codepoint
+    if start_codepoint >= 0:
+        yield (start_codepoint, end_codepoint, range_width)
+
+
+def main() -> None:
+    table_path = join(dirname(__file__), "..", "src", "black", "_width_table.py")
+    with open(table_path, "w") as f:
+        f.write(f"""# Generated by {basename(__file__)}
+# wcwidth {wcwidth.__version__}
+# Unicode {wcwidth.list_versions()[-1]}
+from typing import Final, List, Tuple
+
+WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [
+""")
+        for triple in make_width_table():
+            f.write(f"    {triple!r},\n")
+        f.write("]\n")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.vim/bundle/black/scripts/migrate-black.py b/.vim/bundle/black/scripts/migrate-black.py
new file mode 100755 (executable)
index 0000000..ff52939
--- /dev/null
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+# check out every commit added by the current branch, blackify them,
+# and generate diffs to reconstruct the original commits, but then
+# blackified
+import logging
+import os
+import sys
+from subprocess import PIPE, Popen, check_output, run
+
+
+def git(*args: str) -> str:
+    return check_output(["git"] + list(args)).decode("utf8").strip()
+
+
+def blackify(base_branch: str, black_command: str, logger: logging.Logger) -> int:
+    current_branch = git("branch", "--show-current")
+
+    if not current_branch or base_branch == current_branch:
+        logger.error("You need to check out a feature branch to work on")
+        return 1
+
+    if not os.path.exists(".git"):
+        logger.error("Run me in the root of your repo")
+        return 1
+
+    merge_base = git("merge-base", "HEAD", base_branch)
+    if not merge_base:
+        logger.error(
+            "Could not find a common commit for current head and %s" % base_branch
+        )
+        return 1
+
+    commits = git(
+        "log", "--reverse", "--pretty=format:%H", "%s~1..HEAD" % merge_base
+    ).split()
+    for commit in commits:
+        git("checkout", commit, "-b%s-black" % commit)
+        check_output(black_command, shell=True)
+        git("commit", "-aqm", "blackify")
+
+    git("checkout", base_branch, "-b%s-black" % current_branch)
+
+    for last_commit, commit in zip(commits, commits[1:]):
+        allow_empty = (
+            b"--allow-empty" in run(["git", "apply", "-h"], stdout=PIPE).stdout
+        )
+        quiet = b"--quiet" in run(["git", "apply", "-h"], stdout=PIPE).stdout
+        git_diff = Popen(
+            [
+                "git",
+                "diff",
+                "--binary",
+                "--find-copies",
+                "%s-black..%s-black" % (last_commit, commit),
+            ],
+            stdout=PIPE,
+        )
+        git_apply = Popen(
+            [
+                "git",
+                "apply",
+            ]
+            + (["--quiet"] if quiet else [])
+            + [
+                "-3",
+                "--intent-to-add",
+            ]
+            + (["--allow-empty"] if allow_empty else [])
+            + [
+                "-",
+            ],
+            stdin=git_diff.stdout,
+        )
+        if git_diff.stdout is not None:
+            git_diff.stdout.close()
+        git_apply.communicate()
+        git("commit", "--allow-empty", "-aqC", commit)
+
+    for commit in commits:
+        git("branch", "-qD", "%s-black" % commit)
+
+    return 0
+
+
+if __name__ == "__main__":
+    import argparse
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("base_branch")
+    parser.add_argument("--black_command", default="black -q .")
+    parser.add_argument("--logfile", type=argparse.FileType("w"), default=sys.stdout)
+    args = parser.parse_args()
+    logger = logging.getLogger(__name__)
+    logger.addHandler(logging.StreamHandler(args.logfile))
+    logger.setLevel(logging.INFO)
+    sys.exit(blackify(args.base_branch, args.black_command, logger))
diff --git a/.vim/bundle/black/setup.cfg b/.vim/bundle/black/setup.cfg
deleted file mode 100644 (file)
index 1a0a217..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-[options]
-setup_requires =
-  setuptools_scm[toml]>=6.3.1
diff --git a/.vim/bundle/black/setup.py b/.vim/bundle/black/setup.py
deleted file mode 100644 (file)
index de84dc3..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-# Copyright (C) 2020 Łukasz Langa
-from setuptools import setup, find_packages
-import sys
-import os
-
-assert sys.version_info >= (3, 6, 2), "black requires Python 3.6.2+"
-from pathlib import Path  # noqa E402
-
-CURRENT_DIR = Path(__file__).parent
-sys.path.insert(0, str(CURRENT_DIR))  # for setuptools.build_meta
-
-
-def get_long_description() -> str:
-    return (
-        (CURRENT_DIR / "README.md").read_text(encoding="utf8")
-        + "\n\n"
-        + (CURRENT_DIR / "CHANGES.md").read_text(encoding="utf8")
-    )
-
-
-USE_MYPYC = False
-# To compile with mypyc, a mypyc checkout must be present on the PYTHONPATH
-if len(sys.argv) > 1 and sys.argv[1] == "--use-mypyc":
-    sys.argv.pop(1)
-    USE_MYPYC = True
-if os.getenv("BLACK_USE_MYPYC", None) == "1":
-    USE_MYPYC = True
-
-if USE_MYPYC:
-    mypyc_targets = [
-        "src/black/__init__.py",
-        "src/blib2to3/pytree.py",
-        "src/blib2to3/pygram.py",
-        "src/blib2to3/pgen2/parse.py",
-        "src/blib2to3/pgen2/grammar.py",
-        "src/blib2to3/pgen2/token.py",
-        "src/blib2to3/pgen2/driver.py",
-        "src/blib2to3/pgen2/pgen.py",
-    ]
-
-    from mypyc.build import mypycify
-
-    opt_level = os.getenv("MYPYC_OPT_LEVEL", "3")
-    ext_modules = mypycify(mypyc_targets, opt_level=opt_level)
-else:
-    ext_modules = []
-
-setup(
-    name="black",
-    use_scm_version={
-        "write_to": "src/_black_version.py",
-        "write_to_template": 'version = "{version}"\n',
-    },
-    description="The uncompromising code formatter.",
-    long_description=get_long_description(),
-    long_description_content_type="text/markdown",
-    keywords="automation formatter yapf autopep8 pyfmt gofmt rustfmt",
-    author="Łukasz Langa",
-    author_email="lukasz@langa.pl",
-    url="https://github.com/psf/black",
-    project_urls={"Changelog": "https://github.com/psf/black/blob/main/CHANGES.md"},
-    license="MIT",
-    py_modules=["_black_version"],
-    ext_modules=ext_modules,
-    packages=find_packages(where="src"),
-    package_dir={"": "src"},
-    package_data={
-        "blib2to3": ["*.txt"],
-        "black": ["py.typed"],
-        "black_primer": ["primer.json"],
-    },
-    python_requires=">=3.6.2",
-    zip_safe=False,
-    install_requires=[
-        "click>=7.1.2",
-        "platformdirs>=2",
-        "tomli>=0.2.6,<2.0.0",
-        "typed-ast>=1.4.2; python_version < '3.8'",
-        "regex>=2020.1.8",
-        "pathspec>=0.9.0, <1",
-        "dataclasses>=0.6; python_version < '3.7'",
-        "typing_extensions>=3.10.0.0",
-        # 3.10.0.1 is broken on at least Python 3.10,
-        # https://github.com/python/typing/issues/865
-        "typing_extensions!=3.10.0.1; python_version >= '3.10'",
-        "mypy_extensions>=0.4.3",
-    ],
-    extras_require={
-        "d": ["aiohttp>=3.7.4"],
-        "colorama": ["colorama>=0.4.3"],
-        "python2": ["typed-ast>=1.4.3"],
-        "uvloop": ["uvloop>=0.15.2"],
-        "jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"],
-    },
-    test_suite="tests.test_black",
-    classifiers=[
-        "Development Status :: 4 - Beta",
-        "Environment :: Console",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Programming Language :: Python",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3 :: Only",
-        "Topic :: Software Development :: Libraries :: Python Modules",
-        "Topic :: Software Development :: Quality Assurance",
-    ],
-    entry_points={
-        "console_scripts": [
-            "black=black:patched_main",
-            "blackd=blackd:patched_main [d]",
-            "black-primer=black_primer.cli:main",
-        ]
-    },
-)
index fdbaf040d642b0a41d15d0f98112090971425643..188a4f79f0e51f027f445525bda3b2e68fd93081 100644 (file)
@@ -1,20 +1,16 @@
-import asyncio
-from json.decoder import JSONDecodeError
-import json
-from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor
-from contextlib import contextmanager
-from datetime import datetime
-from enum import Enum
 import io
-from multiprocessing import Manager, freeze_support
-import os
-from pathlib import Path
-from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
-import regex as re
-import signal
+import json
+import platform
+import re
 import sys
 import tokenize
 import traceback
+from contextlib import contextmanager
+from dataclasses import replace
+from datetime import datetime, timezone
+from enum import Enum
+from json.decoder import JSONDecodeError
+from pathlib import Path
 from typing import (
     Any,
     Dict,
@@ -24,47 +20,68 @@ from typing import (
     MutableMapping,
     Optional,
     Pattern,
+    Sequence,
     Set,
     Sized,
     Tuple,
     Union,
 )
 
-from dataclasses import replace
 import click
+from click.core import ParameterSource
+from mypy_extensions import mypyc_attr
+from pathspec import PathSpec
+from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
 
-from black.const import DEFAULT_LINE_LENGTH, DEFAULT_INCLUDES, DEFAULT_EXCLUDES
-from black.const import STDIN_PLACEHOLDER
-from black.nodes import STARS, syms, is_simple_decorator_expression
-from black.lines import Line, EmptyLineTracker
-from black.linegen import transform_line, LineGenerator, LN
+from _black_version import version as __version__
+from black.cache import Cache
 from black.comments import normalize_fmt_off
-from black.mode import Mode, TargetVersion
-from black.mode import Feature, supports_feature, VERSION_TO_FEATURES
-from black.cache import read_cache, write_cache, get_cache_info, filter_cached, Cache
-from black.concurrency import cancel, shutdown, maybe_install_uvloop
-from black.output import dump_to_file, ipynb_diff, diff, color_diff, out, err
-from black.report import Report, Changed, NothingChanged
-from black.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
-from black.files import gen_python_files, get_gitignore, normalize_path_maybe_ignore
-from black.files import wrap_stream_for_windows
-from black.parsing import InvalidInput  # noqa F401
-from black.parsing import lib2to3_parse, parse_ast, stringify_ast
+from black.const import (
+    DEFAULT_EXCLUDES,
+    DEFAULT_INCLUDES,
+    DEFAULT_LINE_LENGTH,
+    STDIN_PLACEHOLDER,
+)
+from black.files import (
+    find_project_root,
+    find_pyproject_toml,
+    find_user_pyproject_toml,
+    gen_python_files,
+    get_gitignore,
+    normalize_path_maybe_ignore,
+    parse_pyproject_toml,
+    wrap_stream_for_windows,
+)
 from black.handle_ipynb_magics import (
-    mask_cell,
-    unmask_cell,
-    remove_trailing_semicolon,
-    put_trailing_semicolon_back,
+    PYTHON_CELL_MAGICS,
     TRANSFORMED_MAGICS,
     jupyter_dependencies_are_installed,
+    mask_cell,
+    put_trailing_semicolon_back,
+    remove_trailing_semicolon,
+    unmask_cell,
 )
-
-
-# lib2to3 fork
-from blib2to3.pytree import Node, Leaf
+from black.linegen import LN, LineGenerator, transform_line
+from black.lines import EmptyLineTracker, LinesBlock
+from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature
+from black.mode import Mode as Mode  # re-exported
+from black.mode import TargetVersion, supports_feature
+from black.nodes import (
+    STARS,
+    is_number_token,
+    is_simple_decorator_expression,
+    is_string_token,
+    syms,
+)
+from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out
+from black.parsing import InvalidInput  # noqa F401
+from black.parsing import lib2to3_parse, parse_ast, stringify_ast
+from black.report import Changed, NothingChanged, Report
+from black.trans import iter_fexpr_spans
 from blib2to3.pgen2 import token
+from blib2to3.pytree import Leaf, Node
 
-from _black_version import version as __version__
+COMPILED = Path(__file__).suffix in (".pyd", ".so")
 
 # types
 FileContent = str
@@ -95,8 +112,6 @@ class WriteBack(Enum):
 # Legacy name, left for integrations.
 FileMode = Mode
 
-DEFAULT_WORKERS = os.cpu_count()
-
 
 def read_pyproject_toml(
     ctx: click.Context, param: click.Parameter, value: Optional[str]
@@ -107,7 +122,9 @@ def read_pyproject_toml(
     otherwise.
     """
     if not value:
-        value = find_pyproject_toml(ctx.params.get("src", ()))
+        value = find_pyproject_toml(
+            ctx.params.get("src", ()), ctx.params.get("stdin_filename", None)
+        )
         if value is None:
             return None
 
@@ -135,6 +152,16 @@ def read_pyproject_toml(
             "target-version", "Config key target-version must be a list"
         )
 
+    exclude = config.get("exclude")
+    if exclude is not None and not isinstance(exclude, str):
+        raise click.BadOptionUsage("exclude", "Config key exclude must be a string")
+
+    extend_exclude = config.get("extend_exclude")
+    if extend_exclude is not None and not isinstance(extend_exclude, str):
+        raise click.BadOptionUsage(
+            "extend-exclude", "Config key extend-exclude must be a string"
+        )
+
     default_map: Dict[str, Any] = {}
     if ctx.default_map:
         default_map.update(ctx.default_map)
@@ -170,14 +197,19 @@ def validate_regex(
     ctx: click.Context,
     param: click.Parameter,
     value: Optional[str],
-) -> Optional[Pattern]:
+) -> Optional[Pattern[str]]:
     try:
         return re_compile_maybe_verbose(value) if value is not None else None
-    except re.error:
-        raise click.BadParameter("Not a valid regular expression") from None
+    except re.error as e:
+        raise click.BadParameter(f"Not a valid regular expression: {e}") from None
 
 
-@click.command(context_settings=dict(help_option_names=["-h", "--help"]))
+@click.command(
+    context_settings={"help_option_names": ["-h", "--help"]},
+    # While Click does set this field automatically using the docstring, mypyc
+    # (annoyingly) strips 'em so we need to set it here too.
+    help="The uncompromising code formatter.",
+)
 @click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
 @click.option(
     "-l",
@@ -194,8 +226,9 @@ def validate_regex(
     callback=target_version_option_callback,
     multiple=True,
     help=(
-        "Python versions that should be supported by Black's output. [default: per-file"
-        " auto-detection]"
+        "Python versions that should be supported by Black's output. By default, Black"
+        " will try to infer this from the project metadata in pyproject.toml. If this"
+        " does not yield conclusive results, Black will use per-file auto-detection."
     ),
 )
 @click.option(
@@ -214,6 +247,22 @@ def validate_regex(
         "(useful when piping source on standard input)."
     ),
 )
+@click.option(
+    "--python-cell-magics",
+    multiple=True,
+    help=(
+        "When processing Jupyter Notebooks, add the given magic to the list"
+        f" of known python-magics ({', '.join(sorted(PYTHON_CELL_MAGICS))})."
+        " Useful for formatting cells with custom python magics."
+    ),
+    default=[],
+)
+@click.option(
+    "-x",
+    "--skip-source-first-line",
+    is_flag=True,
+    help="Skip the first line of the source code.",
+)
 @click.option(
     "-S",
     "--skip-string-normalization",
@@ -230,9 +279,14 @@ def validate_regex(
     "--experimental-string-processing",
     is_flag=True,
     hidden=True,
+    help="(DEPRECATED and now included in --preview) Normalize string literals.",
+)
+@click.option(
+    "--preview",
+    is_flag=True,
     help=(
-        "Experimental option that performs more normalization on string literals."
-        " Currently disabled because it leads to some crashes."
+        "Enable potentially disruptive style changes that may be added to Black's main"
+        " functionality in the next major release."
     ),
 )
 @click.option(
@@ -264,7 +318,8 @@ def validate_regex(
     type=str,
     help=(
         "Require a specific version of Black to be running (useful for unifying results"
-        " across many environments e.g. with a pyproject.toml file)."
+        " across many environments e.g. with a pyproject.toml file). It can be"
+        " either a major version number or an exact version."
     ),
 )
 @click.option(
@@ -314,6 +369,7 @@ def validate_regex(
 @click.option(
     "--stdin-filename",
     type=str,
+    is_eager=True,
     help=(
         "The name of the file when passing it through stdin. Useful to make "
         "sure Black will respect --force-exclude option on some "
@@ -324,9 +380,11 @@ def validate_regex(
     "-W",
     "--workers",
     type=click.IntRange(min=1),
-    default=DEFAULT_WORKERS,
-    show_default=True,
-    help="Number of parallel workers",
+    default=None,
+    help=(
+        "Number of parallel workers [default: BLACK_NUM_WORKERS environment variable "
+        "or number of CPUs in the system]"
+    ),
 )
 @click.option(
     "-q",
@@ -346,7 +404,13 @@ def validate_regex(
         " due to exclusion patterns."
     ),
 )
-@click.version_option(version=__version__)
+@click.version_option(
+    version=__version__,
+    message=(
+        f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n"
+        f"Python ({platform.python_implementation()}) {platform.python_version()}"
+    ),
+)
 @click.argument(
     "src",
     nargs=-1,
@@ -371,7 +435,7 @@ def validate_regex(
     help="Read configuration from FILE path.",
 )
 @click.pass_context
-def main(
+def main(  # noqa: C901
     ctx: click.Context,
     code: Optional[str],
     line_length: int,
@@ -382,27 +446,75 @@ def main(
     fast: bool,
     pyi: bool,
     ipynb: bool,
+    python_cell_magics: Sequence[str],
+    skip_source_first_line: bool,
     skip_string_normalization: bool,
     skip_magic_trailing_comma: bool,
     experimental_string_processing: bool,
+    preview: bool,
     quiet: bool,
     verbose: bool,
-    required_version: str,
-    include: Pattern,
-    exclude: Optional[Pattern],
-    extend_exclude: Optional[Pattern],
-    force_exclude: Optional[Pattern],
+    required_version: Optional[str],
+    include: Pattern[str],
+    exclude: Optional[Pattern[str]],
+    extend_exclude: Optional[Pattern[str]],
+    force_exclude: Optional[Pattern[str]],
     stdin_filename: Optional[str],
-    workers: int,
+    workers: Optional[int],
     src: Tuple[str, ...],
     config: Optional[str],
 ) -> None:
     """The uncompromising code formatter."""
-    if config and verbose:
-        out(f"Using configuration from {config}.", bold=False, fg="blue")
+    ctx.ensure_object(dict)
+
+    if src and code is not None:
+        out(
+            main.get_usage(ctx)
+            + "\n\n'SRC' and 'code' cannot be passed simultaneously."
+        )
+        ctx.exit(1)
+    if not src and code is None:
+        out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.")
+        ctx.exit(1)
+
+    root, method = (
+        find_project_root(src, stdin_filename) if code is None else (None, None)
+    )
+    ctx.obj["root"] = root
+
+    if verbose:
+        if root:
+            out(
+                f"Identified `{root}` as project root containing a {method}.",
+                fg="blue",
+            )
+
+        if config:
+            config_source = ctx.get_parameter_source("config")
+            user_level_config = str(find_user_pyproject_toml())
+            if config == user_level_config:
+                out(
+                    "Using configuration from user-level config at "
+                    f"'{user_level_config}'.",
+                    fg="blue",
+                )
+            elif config_source in (
+                ParameterSource.DEFAULT,
+                ParameterSource.DEFAULT_MAP,
+            ):
+                out("Using configuration from project root.", fg="blue")
+            else:
+                out(f"Using configuration in '{config}'.", fg="blue")
+            if ctx.default_map:
+                for param, value in ctx.default_map.items():
+                    out(f"{param}: {value}")
 
     error_msg = "Oh no! 💥 💔 💥"
-    if required_version and required_version != __version__:
+    if (
+        required_version
+        and required_version != __version__
+        and required_version != __version__.split(".")[0]
+    ):
         err(
             f"{error_msg} The required version `{required_version}` does not match"
             f" the running version `{__version__}`!"
@@ -423,9 +535,12 @@ def main(
         line_length=line_length,
         is_pyi=pyi,
         is_ipynb=ipynb,
+        skip_source_first_line=skip_source_first_line,
         string_normalization=not skip_string_normalization,
         magic_trailing_comma=not skip_magic_trailing_comma,
         experimental_string_processing=experimental_string_processing,
+        preview=preview,
+        python_cell_magics=set(python_cell_magics),
     )
 
     if code is not None:
@@ -440,9 +555,10 @@ def main(
             content=code, fast=fast, write_back=write_back, mode=mode, report=report
         )
     else:
+        assert root is not None  # root is only None if code is not None
         try:
             sources = get_sources(
-                ctx=ctx,
+                root=root,
                 src=src,
                 quiet=quiet,
                 verbose=verbose,
@@ -473,6 +589,8 @@ def main(
                 report=report,
             )
         else:
+            from black.concurrency import reformat_many
+
             reformat_many(
                 sources=sources,
                 fast=fast,
@@ -483,6 +601,8 @@ def main(
             )
 
     if verbose or not quiet:
+        if code is None and (verbose or report.change_count or report.failure_count):
+            out()
         out(error_msg if report.return_code else "All done! ✨ 🍰 ✨")
         if code is None:
             click.echo(str(report), err=True)
@@ -491,7 +611,7 @@ def main(
 
 def get_sources(
     *,
-    ctx: click.Context,
+    root: Path,
     src: Tuple[str, ...],
     quiet: bool,
     verbose: bool,
@@ -503,16 +623,12 @@ def get_sources(
     stdin_filename: Optional[str],
 ) -> Set[Path]:
     """Compute the set of files to be formatted."""
-
-    root = find_project_root(src)
     sources: Set[Path] = set()
-    path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx)
 
-    if exclude is None:
-        exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES)
-        gitignore = get_gitignore(root)
-    else:
-        gitignore = None
+    using_default_exclude = exclude is None
+    exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude
+    gitignore: Optional[Dict[Path, PathSpec]] = None
+    root_gitignore = get_gitignore(root)
 
     for s in src:
         if s == "-" and stdin_filename:
@@ -523,9 +639,15 @@ def get_sources(
             is_stdin = False
 
         if is_stdin or p.is_file():
-            normalized_path = normalize_path_maybe_ignore(p, root, report)
+            normalized_path: Optional[str] = normalize_path_maybe_ignore(
+                p, root, report
+            )
             if normalized_path is None:
+                if verbose:
+                    out(f'Skipping invalid source: "{normalized_path}"', fg="red")
                 continue
+            if verbose:
+                out(f'Found input source: "{normalized_path}"', fg="blue")
 
             normalized_path = "/" + normalized_path
             # Hard-exclude any files that matches the `--force-exclude` regex.
@@ -541,12 +663,23 @@ def get_sources(
                 p = Path(f"{STDIN_PLACEHOLDER}{str(p)}")
 
             if p.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
-                verbose=verbose, quiet=quiet
+                warn=verbose or not quiet
             ):
                 continue
 
             sources.add(p)
         elif p.is_dir():
+            p_relative = normalize_path_maybe_ignore(p, root, report)
+            assert p_relative is not None
+            p = root / p_relative
+            if verbose:
+                out(f'Found input source directory: "{p}"', fg="blue")
+
+            if using_default_exclude:
+                gitignore = {
+                    root: root_gitignore,
+                    p: get_gitignore(p),
+                }
             sources.update(
                 gen_python_files(
                     p.iterdir(),
@@ -562,9 +695,12 @@ def get_sources(
                 )
             )
         elif s == "-":
+            if verbose:
+                out("Found input source stdin", fg="blue")
             sources.add(p)
         else:
             err(f"invalid path: {s}")
+
     return sources
 
 
@@ -604,6 +740,9 @@ def reformat_code(
         report.failed(path, str(exc))
 
 
+# diff-shades depends on being to monkeypatch this function to operate. I know it's
+# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
+@mypyc_attr(patchable=True)
 def reformat_one(
     src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
 ) -> None:
@@ -633,12 +772,9 @@ def reformat_one(
             if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode):
                 changed = Changed.YES
         else:
-            cache: Cache = {}
+            cache = Cache.read(mode)
             if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
-                cache = read_cache(mode)
-                res_src = src.resolve()
-                res_src_s = str(res_src)
-                if res_src_s in cache and cache[res_src_s] == get_cache_info(res_src):
+                if not cache.is_changed(src):
                     changed = Changed.CACHED
             if changed is not Changed.CACHED and format_file_in_place(
                 src, fast=fast, write_back=write_back, mode=mode
@@ -647,7 +783,7 @@ def reformat_one(
             if (write_back is WriteBack.YES and changed is not Changed.CACHED) or (
                 write_back is WriteBack.CHECK and changed is Changed.NO
             ):
-                write_cache(cache, [src], mode)
+                cache.write([src])
         report.done(src, changed)
     except Exception as exc:
         if report.verbose:
@@ -655,119 +791,6 @@ def reformat_one(
         report.failed(src, str(exc))
 
 
-def reformat_many(
-    sources: Set[Path],
-    fast: bool,
-    write_back: WriteBack,
-    mode: Mode,
-    report: "Report",
-    workers: Optional[int],
-) -> None:
-    """Reformat multiple files using a ProcessPoolExecutor."""
-    executor: Executor
-    loop = asyncio.get_event_loop()
-    worker_count = workers if workers is not None else DEFAULT_WORKERS
-    if sys.platform == "win32":
-        # Work around https://bugs.python.org/issue26903
-        worker_count = min(worker_count, 60)
-    try:
-        executor = ProcessPoolExecutor(max_workers=worker_count)
-    except (ImportError, OSError):
-        # we arrive here if the underlying system does not support multi-processing
-        # like in AWS Lambda or Termux, in which case we gracefully fallback to
-        # a ThreadPoolExecutor with just a single worker (more workers would not do us
-        # any good due to the Global Interpreter Lock)
-        executor = ThreadPoolExecutor(max_workers=1)
-
-    try:
-        loop.run_until_complete(
-            schedule_formatting(
-                sources=sources,
-                fast=fast,
-                write_back=write_back,
-                mode=mode,
-                report=report,
-                loop=loop,
-                executor=executor,
-            )
-        )
-    finally:
-        shutdown(loop)
-        if executor is not None:
-            executor.shutdown()
-
-
-async def schedule_formatting(
-    sources: Set[Path],
-    fast: bool,
-    write_back: WriteBack,
-    mode: Mode,
-    report: "Report",
-    loop: asyncio.AbstractEventLoop,
-    executor: Executor,
-) -> None:
-    """Run formatting of `sources` in parallel using the provided `executor`.
-
-    (Use ProcessPoolExecutors for actual parallelism.)
-
-    `write_back`, `fast`, and `mode` options are passed to
-    :func:`format_file_in_place`.
-    """
-    cache: Cache = {}
-    if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
-        cache = read_cache(mode)
-        sources, cached = filter_cached(cache, sources)
-        for src in sorted(cached):
-            report.done(src, Changed.CACHED)
-    if not sources:
-        return
-
-    cancelled = []
-    sources_to_cache = []
-    lock = None
-    if write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
-        # For diff output, we need locks to ensure we don't interleave output
-        # from different processes.
-        manager = Manager()
-        lock = manager.Lock()
-    tasks = {
-        asyncio.ensure_future(
-            loop.run_in_executor(
-                executor, format_file_in_place, src, fast, mode, write_back, lock
-            )
-        ): src
-        for src in sorted(sources)
-    }
-    pending = tasks.keys()
-    try:
-        loop.add_signal_handler(signal.SIGINT, cancel, pending)
-        loop.add_signal_handler(signal.SIGTERM, cancel, pending)
-    except NotImplementedError:
-        # There are no good alternatives for these on Windows.
-        pass
-    while pending:
-        done, _ = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
-        for task in done:
-            src = tasks.pop(task)
-            if task.cancelled():
-                cancelled.append(task)
-            elif task.exception():
-                report.failed(src, str(task.exception()))
-            else:
-                changed = Changed.YES if task.result() else Changed.NO
-                # If the file was written back or was successfully checked as
-                # well-formatted, store this information in the cache.
-                if write_back is WriteBack.YES or (
-                    write_back is WriteBack.CHECK and changed is Changed.NO
-                ):
-                    sources_to_cache.append(src)
-                report.done(src, changed)
-    if cancelled:
-        await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
-    if sources_to_cache:
-        write_cache(cache, sources_to_cache, mode)
-
-
 def format_file_in_place(
     src: Path,
     fast: bool,
@@ -786,8 +809,11 @@ def format_file_in_place(
     elif src.suffix == ".ipynb":
         mode = replace(mode, is_ipynb=True)
 
-    then = datetime.utcfromtimestamp(src.stat().st_mtime)
+    then = datetime.fromtimestamp(src.stat().st_mtime, timezone.utc)
+    header = b""
     with open(src, "rb") as buf:
+        if mode.skip_source_first_line:
+            header = buf.readline()
         src_contents, encoding, newline = decode_bytes(buf.read())
     try:
         dst_contents = format_file_contents(src_contents, fast=fast, mode=mode)
@@ -797,14 +823,16 @@ def format_file_in_place(
         raise ValueError(
             f"File '{src}' cannot be parsed as valid Jupyter notebook."
         ) from None
+    src_contents = header.decode(encoding) + src_contents
+    dst_contents = header.decode(encoding) + dst_contents
 
     if write_back == WriteBack.YES:
         with open(src, "w", encoding=encoding, newline=newline) as f:
             f.write(dst_contents)
     elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
-        now = datetime.utcnow()
-        src_name = f"{src}\t{then} +0000"
-        dst_name = f"{src}\t{now} +0000"
+        now = datetime.now(timezone.utc)
+        src_name = f"{src}\t{then}"
+        dst_name = f"{src}\t{now}"
         if mode.is_ipynb:
             diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name)
         else:
@@ -842,7 +870,7 @@ def format_stdin_to_stdout(
     write a diff to stdout. The `mode` argument is passed to
     :func:`format_file_contents`.
     """
-    then = datetime.utcnow()
+    then = datetime.now(timezone.utc)
 
     if content is None:
         src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
@@ -867,9 +895,9 @@ def format_stdin_to_stdout(
                 dst += "\n"
             f.write(dst)
         elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
-            now = datetime.utcnow()
-            src_name = f"STDIN\t{then} +0000"
-            dst_name = f"STDOUT\t{now} +0000"
+            now = datetime.now(timezone.utc)
+            src_name = f"STDIN\t{then}"
+            dst_name = f"STDOUT\t{now}"
             d = diff(src, dst, src_name, dst_name)
             if write_back == WriteBack.COLOR_DIFF:
                 d = color_diff(d)
@@ -888,17 +916,7 @@ def check_stability_and_equivalence(
     content differently.
     """
     assert_equivalent(src_contents, dst_contents)
-
-    # Forced second pass to work around optional trailing commas (becoming
-    # forced trailing commas on pass 2) interacting differently with optional
-    # parentheses.  Admittedly ugly.
-    dst_contents_pass2 = format_str(dst_contents, mode=mode)
-    if dst_contents != dst_contents_pass2:
-        dst_contents = dst_contents_pass2
-        assert_equivalent(src_contents, dst_contents, pass_num=2)
-        assert_stable(src_contents, dst_contents, mode=mode)
-    # Note: no need to explicitly call `assert_stable` if `dst_contents` was
-    # the same as `dst_contents_pass2`.
+    assert_stable(src_contents, dst_contents, mode=mode)
 
 
 def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
@@ -908,9 +926,6 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo
     valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
     `mode` is passed to :func:`format_str`.
     """
-    if not src_contents.strip():
-        raise NothingChanged
-
     if mode.is_ipynb:
         dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode)
     else:
@@ -924,8 +939,10 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo
     return dst_contents
 
 
-def validate_cell(src: str) -> None:
-    """Check that cell does not already contain TransformerManager transformations.
+def validate_cell(src: str, mode: Mode) -> None:
+    """Check that cell does not already contain TransformerManager transformations,
+    or non-Python cell magics, which might cause tokenizer_rt to break because of
+    indentations.
 
     If a cell contains ``!ls``, then it'll be transformed to
     ``get_ipython().system('ls')``. However, if the cell originally contained
@@ -941,6 +958,11 @@ def validate_cell(src: str) -> None:
     """
     if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS):
         raise NothingChanged
+    if (
+        src[:2] == "%%"
+        and src.split()[0][2:] not in PYTHON_CELL_MAGICS | mode.python_cell_magics
+    ):
+        raise NothingChanged
 
 
 def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
@@ -959,7 +981,7 @@ def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
     could potentially be automagics or multi-line magics, which
     are currently not supported.
     """
-    validate_cell(src)
+    validate_cell(src, mode)
     src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon(
         src
     )
@@ -998,6 +1020,9 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon
     Operate cell-by-cell, only on code cells, only for Python notebooks.
     If the ``.ipynb`` originally had a trailing newline, it'll be preserved.
     """
+    if not src_contents:
+        raise NothingChanged
+
     trailing_newline = src_contents[-1] == "\n"
     modified = False
     nb = json.loads(src_contents)
@@ -1021,7 +1046,7 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon
         raise NothingChanged
 
 
-def format_str(src_contents: str, *, mode: Mode) -> FileContent:
+def format_str(src_contents: str, *, mode: Mode) -> str:
     """Reformat a string and return new contents.
 
     `mode` determines formatting options, such as how many characters per line are
@@ -1051,35 +1076,57 @@ def format_str(src_contents: str, *, mode: Mode) -> FileContent:
         hey
 
     """
+    dst_contents = _format_str_once(src_contents, mode=mode)
+    # Forced second pass to work around optional trailing commas (becoming
+    # forced trailing commas on pass 2) interacting differently with optional
+    # parentheses.  Admittedly ugly.
+    if src_contents != dst_contents:
+        return _format_str_once(dst_contents, mode=mode)
+    return dst_contents
+
+
+def _format_str_once(src_contents: str, *, mode: Mode) -> str:
     src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
-    dst_contents = []
-    future_imports = get_future_imports(src_node)
+    dst_blocks: List[LinesBlock] = []
     if mode.target_versions:
         versions = mode.target_versions
     else:
-        versions = detect_target_versions(src_node)
+        future_imports = get_future_imports(src_node)
+        versions = detect_target_versions(src_node, future_imports=future_imports)
+
+    context_manager_features = {
+        feature
+        for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS}
+        if supports_feature(versions, feature)
+    }
     normalize_fmt_off(src_node)
-    lines = LineGenerator(
-        mode=mode,
-        remove_u_prefix="unicode_literals" in future_imports
-        or supports_feature(versions, Feature.UNICODE_LITERALS),
-    )
-    elt = EmptyLineTracker(is_pyi=mode.is_pyi)
-    empty_line = Line(mode=mode)
-    after = 0
+    lines = LineGenerator(mode=mode, features=context_manager_features)
+    elt = EmptyLineTracker(mode=mode)
     split_line_features = {
         feature
         for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
         if supports_feature(versions, feature)
     }
+    block: Optional[LinesBlock] = None
     for current_line in lines.visit(src_node):
-        dst_contents.append(str(empty_line) * after)
-        before, after = elt.maybe_empty_lines(current_line)
-        dst_contents.append(str(empty_line) * before)
+        block = elt.maybe_empty_lines(current_line)
+        dst_blocks.append(block)
         for line in transform_line(
             current_line, mode=mode, features=split_line_features
         ):
-            dst_contents.append(str(line))
+            block.content_lines.append(str(line))
+    if dst_blocks:
+        dst_blocks[-1].after = 0
+    dst_contents = []
+    for block in dst_blocks:
+        dst_contents.extend(block.all_lines())
+    if not dst_contents:
+        # Use decode_bytes to retrieve the correct source newline (CRLF or LF),
+        # and check if normalized_content has more than one line
+        normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8"))
+        if "\n" in normalized_content:
+            return newline
+        return ""
     return "".join(dst_contents)
 
 
@@ -1100,26 +1147,47 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
         return tiow.read(), encoding, newline
 
 
-def get_features_used(node: Node) -> Set[Feature]:
+def get_features_used(  # noqa: C901
+    node: Node, *, future_imports: Optional[Set[str]] = None
+) -> Set[Feature]:
     """Return a set of (relatively) new Python features used in this file.
 
     Currently looking for:
     - f-strings;
+    - self-documenting expressions in f-strings (f"{x=}");
     - underscores in numeric literals;
     - trailing commas after * or ** in function signatures and calls;
     - positional only arguments in function signatures and lambdas;
     - assignment expression;
     - relaxed decorator syntax;
+    - usage of __future__ flags (annotations);
+    - print / exec statements;
+    - parenthesized context managers;
+    - match statements;
+    - except* clause;
+    - variadic generics;
     """
     features: Set[Feature] = set()
+    if future_imports:
+        features |= {
+            FUTURE_FLAG_TO_FEATURE[future_import]
+            for future_import in future_imports
+            if future_import in FUTURE_FLAG_TO_FEATURE
+        }
+
     for n in node.pre_order():
-        if n.type == token.STRING:
-            value_head = n.value[:2]  # type: ignore
+        if is_string_token(n):
+            value_head = n.value[:2]
             if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
                 features.add(Feature.F_STRINGS)
-
-        elif n.type == token.NUMBER:
-            if "_" in n.value:  # type: ignore
+                if Feature.DEBUG_F_STRINGS not in features:
+                    for span_beg, span_end in iter_fexpr_spans(n.value):
+                        if n.value[span_beg : span_end - 1].rstrip().endswith("="):
+                            features.add(Feature.DEBUG_F_STRINGS)
+                            break
+
+        elif is_number_token(n):
+            if "_" in n.value:
                 features.add(Feature.NUMERIC_UNDERSCORES)
 
         elif n.type == token.SLASH:
@@ -1158,12 +1226,68 @@ def get_features_used(node: Node) -> Set[Feature]:
                         if argch.type in STARS:
                             features.add(feature)
 
+        elif (
+            n.type in {syms.return_stmt, syms.yield_expr}
+            and len(n.children) >= 2
+            and n.children[1].type == syms.testlist_star_expr
+            and any(child.type == syms.star_expr for child in n.children[1].children)
+        ):
+            features.add(Feature.UNPACKING_ON_FLOW)
+
+        elif (
+            n.type == syms.annassign
+            and len(n.children) >= 4
+            and n.children[3].type == syms.testlist_star_expr
+        ):
+            features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
+
+        elif (
+            n.type == syms.with_stmt
+            and len(n.children) > 2
+            and n.children[1].type == syms.atom
+        ):
+            atom_children = n.children[1].children
+            if (
+                len(atom_children) == 3
+                and atom_children[0].type == token.LPAR
+                and atom_children[1].type == syms.testlist_gexp
+                and atom_children[2].type == token.RPAR
+            ):
+                features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS)
+
+        elif n.type == syms.match_stmt:
+            features.add(Feature.PATTERN_MATCHING)
+
+        elif (
+            n.type == syms.except_clause
+            and len(n.children) >= 2
+            and n.children[1].type == token.STAR
+        ):
+            features.add(Feature.EXCEPT_STAR)
+
+        elif n.type in {syms.subscriptlist, syms.trailer} and any(
+            child.type == syms.star_expr for child in n.children
+        ):
+            features.add(Feature.VARIADIC_GENERICS)
+
+        elif (
+            n.type == syms.tname_star
+            and len(n.children) == 3
+            and n.children[2].type == syms.star_expr
+        ):
+            features.add(Feature.VARIADIC_GENERICS)
+
+        elif n.type in (syms.type_stmt, syms.typeparams):
+            features.add(Feature.TYPE_PARAMS)
+
     return features
 
 
-def detect_target_versions(node: Node) -> Set[TargetVersion]:
+def detect_target_versions(
+    node: Node, *, future_imports: Optional[Set[str]] = None
+) -> Set[TargetVersion]:
     """Detect the version to target based on the nodes used."""
-    features = get_features_used(node)
+    features = get_features_used(node, future_imports=future_imports)
     return {
         version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
     }
@@ -1219,13 +1343,16 @@ def get_future_imports(node: Node) -> Set[str]:
     return imports
 
 
-def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None:
+def assert_equivalent(src: str, dst: str) -> None:
     """Raise AssertionError if `src` and `dst` aren't equivalent."""
     try:
         src_ast = parse_ast(src)
     except Exception as exc:
         raise AssertionError(
-            "cannot use --safe with this file; failed to parse source file."
+            "cannot use --safe with this file; failed to parse source file AST: "
+            f"{exc}\n"
+            "This could be caused by running Black with an older Python version "
+            "that does not support new syntax used in your source file."
         ) from exc
 
     try:
@@ -1233,7 +1360,7 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None:
     except Exception as exc:
         log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
         raise AssertionError(
-            f"INTERNAL ERROR: Black produced invalid code on pass {pass_num}: {exc}. "
+            f"INTERNAL ERROR: Black produced invalid code: {exc}. "
             "Please report a bug on https://github.com/psf/black/issues.  "
             f"This invalid output might be helpful: {log}"
         ) from None
@@ -1244,14 +1371,17 @@ def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None:
         log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
         raise AssertionError(
             "INTERNAL ERROR: Black produced code that is not equivalent to the"
-            f" source on pass {pass_num}.  Please report a bug on "
+            " source.  Please report a bug on "
             f"https://github.com/psf/black/issues.  This diff might be helpful: {log}"
         ) from None
 
 
 def assert_stable(src: str, dst: str, mode: Mode) -> None:
     """Raise AssertionError if `dst` reformats differently the second time."""
-    newdst = format_str(dst, mode=mode)
+    # We shouldn't call format_str() here, because that formats the string
+    # twice and may hide a bug where we bounce back and forth between two
+    # versions.
+    newdst = _format_str_once(dst, mode=mode)
     if dst != newdst:
         log = dump_to_file(
             str(mode),
@@ -1274,34 +1404,14 @@ def nullcontext() -> Iterator[None]:
     yield
 
 
-def patch_click() -> None:
-    """Make Click not crash on Python 3.6 with LANG=C.
-
-    On certain misconfigured environments, Python 3 selects the ASCII encoding as the
-    default which restricts paths that it can access during the lifetime of the
-    application.  Click refuses to work in this scenario by raising a RuntimeError.
-
-    In case of Black the likelihood that non-ASCII characters are going to be used in
-    file paths is minimal since it's Python source code.  Moreover, this crash was
-    spurious on Python 3.7 thanks to PEP 538 and PEP 540.
-    """
-    try:
-        from click import core
-        from click import _unicodefun  # type: ignore
-    except ModuleNotFoundError:
-        return
-
-    for module in (core, _unicodefun):
-        if hasattr(module, "_verify_python3_env"):
-            module._verify_python3_env = lambda: None  # type: ignore
-        if hasattr(module, "_verify_python_env"):
-            module._verify_python_env = lambda: None  # type: ignore
+def patched_main() -> None:
+    # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows
+    # environments so just assume we always need to call it if frozen.
+    if getattr(sys, "frozen", False):
+        from multiprocessing import freeze_support
 
+        freeze_support()
 
-def patched_main() -> None:
-    maybe_install_uvloop()
-    freeze_support()
-    patch_click()
     main()
 
 
diff --git a/.vim/bundle/black/src/black/_width_table.py b/.vim/bundle/black/src/black/_width_table.py
new file mode 100644 (file)
index 0000000..f3304e4
--- /dev/null
@@ -0,0 +1,478 @@
+# Generated by make_width_table.py
+# wcwidth 0.2.6
+# Unicode 15.0.0
+from typing import Final, List, Tuple
+
+WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [
+    (0, 0, 0),
+    (1, 31, -1),
+    (127, 159, -1),
+    (768, 879, 0),
+    (1155, 1161, 0),
+    (1425, 1469, 0),
+    (1471, 1471, 0),
+    (1473, 1474, 0),
+    (1476, 1477, 0),
+    (1479, 1479, 0),
+    (1552, 1562, 0),
+    (1611, 1631, 0),
+    (1648, 1648, 0),
+    (1750, 1756, 0),
+    (1759, 1764, 0),
+    (1767, 1768, 0),
+    (1770, 1773, 0),
+    (1809, 1809, 0),
+    (1840, 1866, 0),
+    (1958, 1968, 0),
+    (2027, 2035, 0),
+    (2045, 2045, 0),
+    (2070, 2073, 0),
+    (2075, 2083, 0),
+    (2085, 2087, 0),
+    (2089, 2093, 0),
+    (2137, 2139, 0),
+    (2200, 2207, 0),
+    (2250, 2273, 0),
+    (2275, 2306, 0),
+    (2362, 2362, 0),
+    (2364, 2364, 0),
+    (2369, 2376, 0),
+    (2381, 2381, 0),
+    (2385, 2391, 0),
+    (2402, 2403, 0),
+    (2433, 2433, 0),
+    (2492, 2492, 0),
+    (2497, 2500, 0),
+    (2509, 2509, 0),
+    (2530, 2531, 0),
+    (2558, 2558, 0),
+    (2561, 2562, 0),
+    (2620, 2620, 0),
+    (2625, 2626, 0),
+    (2631, 2632, 0),
+    (2635, 2637, 0),
+    (2641, 2641, 0),
+    (2672, 2673, 0),
+    (2677, 2677, 0),
+    (2689, 2690, 0),
+    (2748, 2748, 0),
+    (2753, 2757, 0),
+    (2759, 2760, 0),
+    (2765, 2765, 0),
+    (2786, 2787, 0),
+    (2810, 2815, 0),
+    (2817, 2817, 0),
+    (2876, 2876, 0),
+    (2879, 2879, 0),
+    (2881, 2884, 0),
+    (2893, 2893, 0),
+    (2901, 2902, 0),
+    (2914, 2915, 0),
+    (2946, 2946, 0),
+    (3008, 3008, 0),
+    (3021, 3021, 0),
+    (3072, 3072, 0),
+    (3076, 3076, 0),
+    (3132, 3132, 0),
+    (3134, 3136, 0),
+    (3142, 3144, 0),
+    (3146, 3149, 0),
+    (3157, 3158, 0),
+    (3170, 3171, 0),
+    (3201, 3201, 0),
+    (3260, 3260, 0),
+    (3263, 3263, 0),
+    (3270, 3270, 0),
+    (3276, 3277, 0),
+    (3298, 3299, 0),
+    (3328, 3329, 0),
+    (3387, 3388, 0),
+    (3393, 3396, 0),
+    (3405, 3405, 0),
+    (3426, 3427, 0),
+    (3457, 3457, 0),
+    (3530, 3530, 0),
+    (3538, 3540, 0),
+    (3542, 3542, 0),
+    (3633, 3633, 0),
+    (3636, 3642, 0),
+    (3655, 3662, 0),
+    (3761, 3761, 0),
+    (3764, 3772, 0),
+    (3784, 3790, 0),
+    (3864, 3865, 0),
+    (3893, 3893, 0),
+    (3895, 3895, 0),
+    (3897, 3897, 0),
+    (3953, 3966, 0),
+    (3968, 3972, 0),
+    (3974, 3975, 0),
+    (3981, 3991, 0),
+    (3993, 4028, 0),
+    (4038, 4038, 0),
+    (4141, 4144, 0),
+    (4146, 4151, 0),
+    (4153, 4154, 0),
+    (4157, 4158, 0),
+    (4184, 4185, 0),
+    (4190, 4192, 0),
+    (4209, 4212, 0),
+    (4226, 4226, 0),
+    (4229, 4230, 0),
+    (4237, 4237, 0),
+    (4253, 4253, 0),
+    (4352, 4447, 2),
+    (4957, 4959, 0),
+    (5906, 5908, 0),
+    (5938, 5939, 0),
+    (5970, 5971, 0),
+    (6002, 6003, 0),
+    (6068, 6069, 0),
+    (6071, 6077, 0),
+    (6086, 6086, 0),
+    (6089, 6099, 0),
+    (6109, 6109, 0),
+    (6155, 6157, 0),
+    (6159, 6159, 0),
+    (6277, 6278, 0),
+    (6313, 6313, 0),
+    (6432, 6434, 0),
+    (6439, 6440, 0),
+    (6450, 6450, 0),
+    (6457, 6459, 0),
+    (6679, 6680, 0),
+    (6683, 6683, 0),
+    (6742, 6742, 0),
+    (6744, 6750, 0),
+    (6752, 6752, 0),
+    (6754, 6754, 0),
+    (6757, 6764, 0),
+    (6771, 6780, 0),
+    (6783, 6783, 0),
+    (6832, 6862, 0),
+    (6912, 6915, 0),
+    (6964, 6964, 0),
+    (6966, 6970, 0),
+    (6972, 6972, 0),
+    (6978, 6978, 0),
+    (7019, 7027, 0),
+    (7040, 7041, 0),
+    (7074, 7077, 0),
+    (7080, 7081, 0),
+    (7083, 7085, 0),
+    (7142, 7142, 0),
+    (7144, 7145, 0),
+    (7149, 7149, 0),
+    (7151, 7153, 0),
+    (7212, 7219, 0),
+    (7222, 7223, 0),
+    (7376, 7378, 0),
+    (7380, 7392, 0),
+    (7394, 7400, 0),
+    (7405, 7405, 0),
+    (7412, 7412, 0),
+    (7416, 7417, 0),
+    (7616, 7679, 0),
+    (8203, 8207, 0),
+    (8232, 8238, 0),
+    (8288, 8291, 0),
+    (8400, 8432, 0),
+    (8986, 8987, 2),
+    (9001, 9002, 2),
+    (9193, 9196, 2),
+    (9200, 9200, 2),
+    (9203, 9203, 2),
+    (9725, 9726, 2),
+    (9748, 9749, 2),
+    (9800, 9811, 2),
+    (9855, 9855, 2),
+    (9875, 9875, 2),
+    (9889, 9889, 2),
+    (9898, 9899, 2),
+    (9917, 9918, 2),
+    (9924, 9925, 2),
+    (9934, 9934, 2),
+    (9940, 9940, 2),
+    (9962, 9962, 2),
+    (9970, 9971, 2),
+    (9973, 9973, 2),
+    (9978, 9978, 2),
+    (9981, 9981, 2),
+    (9989, 9989, 2),
+    (9994, 9995, 2),
+    (10024, 10024, 2),
+    (10060, 10060, 2),
+    (10062, 10062, 2),
+    (10067, 10069, 2),
+    (10071, 10071, 2),
+    (10133, 10135, 2),
+    (10160, 10160, 2),
+    (10175, 10175, 2),
+    (11035, 11036, 2),
+    (11088, 11088, 2),
+    (11093, 11093, 2),
+    (11503, 11505, 0),
+    (11647, 11647, 0),
+    (11744, 11775, 0),
+    (11904, 11929, 2),
+    (11931, 12019, 2),
+    (12032, 12245, 2),
+    (12272, 12283, 2),
+    (12288, 12329, 2),
+    (12330, 12333, 0),
+    (12334, 12350, 2),
+    (12353, 12438, 2),
+    (12441, 12442, 0),
+    (12443, 12543, 2),
+    (12549, 12591, 2),
+    (12593, 12686, 2),
+    (12688, 12771, 2),
+    (12784, 12830, 2),
+    (12832, 12871, 2),
+    (12880, 19903, 2),
+    (19968, 42124, 2),
+    (42128, 42182, 2),
+    (42607, 42610, 0),
+    (42612, 42621, 0),
+    (42654, 42655, 0),
+    (42736, 42737, 0),
+    (43010, 43010, 0),
+    (43014, 43014, 0),
+    (43019, 43019, 0),
+    (43045, 43046, 0),
+    (43052, 43052, 0),
+    (43204, 43205, 0),
+    (43232, 43249, 0),
+    (43263, 43263, 0),
+    (43302, 43309, 0),
+    (43335, 43345, 0),
+    (43360, 43388, 2),
+    (43392, 43394, 0),
+    (43443, 43443, 0),
+    (43446, 43449, 0),
+    (43452, 43453, 0),
+    (43493, 43493, 0),
+    (43561, 43566, 0),
+    (43569, 43570, 0),
+    (43573, 43574, 0),
+    (43587, 43587, 0),
+    (43596, 43596, 0),
+    (43644, 43644, 0),
+    (43696, 43696, 0),
+    (43698, 43700, 0),
+    (43703, 43704, 0),
+    (43710, 43711, 0),
+    (43713, 43713, 0),
+    (43756, 43757, 0),
+    (43766, 43766, 0),
+    (44005, 44005, 0),
+    (44008, 44008, 0),
+    (44013, 44013, 0),
+    (44032, 55203, 2),
+    (63744, 64255, 2),
+    (64286, 64286, 0),
+    (65024, 65039, 0),
+    (65040, 65049, 2),
+    (65056, 65071, 0),
+    (65072, 65106, 2),
+    (65108, 65126, 2),
+    (65128, 65131, 2),
+    (65281, 65376, 2),
+    (65504, 65510, 2),
+    (66045, 66045, 0),
+    (66272, 66272, 0),
+    (66422, 66426, 0),
+    (68097, 68099, 0),
+    (68101, 68102, 0),
+    (68108, 68111, 0),
+    (68152, 68154, 0),
+    (68159, 68159, 0),
+    (68325, 68326, 0),
+    (68900, 68903, 0),
+    (69291, 69292, 0),
+    (69373, 69375, 0),
+    (69446, 69456, 0),
+    (69506, 69509, 0),
+    (69633, 69633, 0),
+    (69688, 69702, 0),
+    (69744, 69744, 0),
+    (69747, 69748, 0),
+    (69759, 69761, 0),
+    (69811, 69814, 0),
+    (69817, 69818, 0),
+    (69826, 69826, 0),
+    (69888, 69890, 0),
+    (69927, 69931, 0),
+    (69933, 69940, 0),
+    (70003, 70003, 0),
+    (70016, 70017, 0),
+    (70070, 70078, 0),
+    (70089, 70092, 0),
+    (70095, 70095, 0),
+    (70191, 70193, 0),
+    (70196, 70196, 0),
+    (70198, 70199, 0),
+    (70206, 70206, 0),
+    (70209, 70209, 0),
+    (70367, 70367, 0),
+    (70371, 70378, 0),
+    (70400, 70401, 0),
+    (70459, 70460, 0),
+    (70464, 70464, 0),
+    (70502, 70508, 0),
+    (70512, 70516, 0),
+    (70712, 70719, 0),
+    (70722, 70724, 0),
+    (70726, 70726, 0),
+    (70750, 70750, 0),
+    (70835, 70840, 0),
+    (70842, 70842, 0),
+    (70847, 70848, 0),
+    (70850, 70851, 0),
+    (71090, 71093, 0),
+    (71100, 71101, 0),
+    (71103, 71104, 0),
+    (71132, 71133, 0),
+    (71219, 71226, 0),
+    (71229, 71229, 0),
+    (71231, 71232, 0),
+    (71339, 71339, 0),
+    (71341, 71341, 0),
+    (71344, 71349, 0),
+    (71351, 71351, 0),
+    (71453, 71455, 0),
+    (71458, 71461, 0),
+    (71463, 71467, 0),
+    (71727, 71735, 0),
+    (71737, 71738, 0),
+    (71995, 71996, 0),
+    (71998, 71998, 0),
+    (72003, 72003, 0),
+    (72148, 72151, 0),
+    (72154, 72155, 0),
+    (72160, 72160, 0),
+    (72193, 72202, 0),
+    (72243, 72248, 0),
+    (72251, 72254, 0),
+    (72263, 72263, 0),
+    (72273, 72278, 0),
+    (72281, 72283, 0),
+    (72330, 72342, 0),
+    (72344, 72345, 0),
+    (72752, 72758, 0),
+    (72760, 72765, 0),
+    (72767, 72767, 0),
+    (72850, 72871, 0),
+    (72874, 72880, 0),
+    (72882, 72883, 0),
+    (72885, 72886, 0),
+    (73009, 73014, 0),
+    (73018, 73018, 0),
+    (73020, 73021, 0),
+    (73023, 73029, 0),
+    (73031, 73031, 0),
+    (73104, 73105, 0),
+    (73109, 73109, 0),
+    (73111, 73111, 0),
+    (73459, 73460, 0),
+    (73472, 73473, 0),
+    (73526, 73530, 0),
+    (73536, 73536, 0),
+    (73538, 73538, 0),
+    (78912, 78912, 0),
+    (78919, 78933, 0),
+    (92912, 92916, 0),
+    (92976, 92982, 0),
+    (94031, 94031, 0),
+    (94095, 94098, 0),
+    (94176, 94179, 2),
+    (94180, 94180, 0),
+    (94192, 94193, 2),
+    (94208, 100343, 2),
+    (100352, 101589, 2),
+    (101632, 101640, 2),
+    (110576, 110579, 2),
+    (110581, 110587, 2),
+    (110589, 110590, 2),
+    (110592, 110882, 2),
+    (110898, 110898, 2),
+    (110928, 110930, 2),
+    (110933, 110933, 2),
+    (110948, 110951, 2),
+    (110960, 111355, 2),
+    (113821, 113822, 0),
+    (118528, 118573, 0),
+    (118576, 118598, 0),
+    (119143, 119145, 0),
+    (119163, 119170, 0),
+    (119173, 119179, 0),
+    (119210, 119213, 0),
+    (119362, 119364, 0),
+    (121344, 121398, 0),
+    (121403, 121452, 0),
+    (121461, 121461, 0),
+    (121476, 121476, 0),
+    (121499, 121503, 0),
+    (121505, 121519, 0),
+    (122880, 122886, 0),
+    (122888, 122904, 0),
+    (122907, 122913, 0),
+    (122915, 122916, 0),
+    (122918, 122922, 0),
+    (123023, 123023, 0),
+    (123184, 123190, 0),
+    (123566, 123566, 0),
+    (123628, 123631, 0),
+    (124140, 124143, 0),
+    (125136, 125142, 0),
+    (125252, 125258, 0),
+    (126980, 126980, 2),
+    (127183, 127183, 2),
+    (127374, 127374, 2),
+    (127377, 127386, 2),
+    (127488, 127490, 2),
+    (127504, 127547, 2),
+    (127552, 127560, 2),
+    (127568, 127569, 2),
+    (127584, 127589, 2),
+    (127744, 127776, 2),
+    (127789, 127797, 2),
+    (127799, 127868, 2),
+    (127870, 127891, 2),
+    (127904, 127946, 2),
+    (127951, 127955, 2),
+    (127968, 127984, 2),
+    (127988, 127988, 2),
+    (127992, 128062, 2),
+    (128064, 128064, 2),
+    (128066, 128252, 2),
+    (128255, 128317, 2),
+    (128331, 128334, 2),
+    (128336, 128359, 2),
+    (128378, 128378, 2),
+    (128405, 128406, 2),
+    (128420, 128420, 2),
+    (128507, 128591, 2),
+    (128640, 128709, 2),
+    (128716, 128716, 2),
+    (128720, 128722, 2),
+    (128725, 128727, 2),
+    (128732, 128735, 2),
+    (128747, 128748, 2),
+    (128756, 128764, 2),
+    (128992, 129003, 2),
+    (129008, 129008, 2),
+    (129292, 129338, 2),
+    (129340, 129349, 2),
+    (129351, 129535, 2),
+    (129648, 129660, 2),
+    (129664, 129672, 2),
+    (129680, 129725, 2),
+    (129727, 129733, 2),
+    (129742, 129755, 2),
+    (129760, 129768, 2),
+    (129776, 129784, 2),
+    (131072, 196605, 2),
+    (196608, 262141, 2),
+    (917760, 917999, 0),
+]
index bb865a0d5b709173ac5147549d41a27563ba1f40..85dac6edd1eaa1e86a7c4f08f09d8a7aec4752dc 100644 (file)
@@ -1,20 +1,22 @@
 """Builds on top of nodes.py to track brackets."""
 
 from dataclasses import dataclass, field
-import sys
-from typing import Dict, Iterable, List, Optional, Tuple, Union
-
-if sys.version_info < (3, 8):
-    from typing_extensions import Final
-else:
-    from typing import Final
-
-from blib2to3.pytree import Leaf, Node
+from typing import Dict, Final, Iterable, List, Optional, Sequence, Set, Tuple, Union
+
+from black.nodes import (
+    BRACKET,
+    CLOSING_BRACKETS,
+    COMPARATORS,
+    LOGIC_OPERATORS,
+    MATH_OPERATORS,
+    OPENING_BRACKETS,
+    UNPACKING_PARENTS,
+    VARARGS_PARENTS,
+    is_vararg,
+    syms,
+)
 from blib2to3.pgen2 import token
-
-from black.nodes import syms, is_vararg, VARARGS_PARENTS, UNPACKING_PARENTS
-from black.nodes import BRACKET, OPENING_BRACKETS, CLOSING_BRACKETS
-from black.nodes import MATH_OPERATORS, COMPARATORS, LOGIC_OPERATORS
+from blib2to3.pytree import Leaf, Node
 
 # types
 LN = Union[Leaf, Node]
@@ -49,7 +51,7 @@ MATH_PRIORITIES: Final = {
 DOT_PRIORITY: Final = 1
 
 
-class BracketMatchError(KeyError):
+class BracketMatchError(Exception):
     """Raised when an opening bracket is unable to be matched to a closing bracket."""
 
 
@@ -72,9 +74,12 @@ class BracketTracker:
         within brackets a given leaf is. 0 means there are no enclosing brackets
         that started on this line.
 
-        If a leaf is itself a closing bracket, it receives an `opening_bracket`
-        field that it forms a pair with. This is a one-directional link to
-        avoid reference cycles.
+        If a leaf is itself a closing bracket and there is a matching opening
+        bracket earlier, it receives an `opening_bracket` field with which it forms a
+        pair. This is a one-directional link to avoid reference cycles. Closing
+        bracket without opening happens on lines continued from previous
+        breaks, e.g. `) -> "ReturnType":` as part of a funcdef where we place
+        the return type annotation on its own line of the previous closing RPAR.
 
         If a leaf is a delimiter (a token on which Black can split the line if
         needed) and it's on depth 0, its `id()` is stored in the tracker's
@@ -83,6 +88,13 @@ class BracketTracker:
         if leaf.type == token.COMMENT:
             return
 
+        if (
+            self.depth == 0
+            and leaf.type in CLOSING_BRACKETS
+            and (self.depth, leaf.type) not in self.bracket_match
+        ):
+            return
+
         self.maybe_decrement_after_for_loop_variable(leaf)
         self.maybe_decrement_after_lambda_arguments(leaf)
         if leaf.type in CLOSING_BRACKETS:
@@ -332,3 +344,32 @@ def max_delimiter_priority_in_atom(node: LN) -> Priority:
 
     except ValueError:
         return 0
+
+
+def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> Set[LeafID]:
+    """Return leaves that are inside matching brackets.
+
+    The input `leaves` can have non-matching brackets at the head or tail parts.
+    Matching brackets are included.
+    """
+    try:
+        # Start with the first opening bracket and ignore closing brackets before.
+        start_index = next(
+            i for i, l in enumerate(leaves) if l.type in OPENING_BRACKETS
+        )
+    except StopIteration:
+        return set()
+    bracket_stack = []
+    ids = set()
+    for i in range(start_index, len(leaves)):
+        leaf = leaves[i]
+        if leaf.type in OPENING_BRACKETS:
+            bracket_stack.append((BRACKET[leaf.type], i))
+        if leaf.type in CLOSING_BRACKETS:
+            if bracket_stack and leaf.type == bracket_stack[-1][0]:
+                _, start = bracket_stack.pop()
+                for j in range(start, i + 1):
+                    ids.add(id(leaves[j]))
+            else:
+                break
+    return ids
index 3f165de2ed60bf2563cee4e662baeb36c3553a40..6baa096bacafccc22f66eecb2bbaee9ad3862b25 100644 (file)
 """Caching of formatted files with feature-based invalidation."""
 
+import hashlib
 import os
 import pickle
-from pathlib import Path
+import sys
 import tempfile
-from typing import Dict, Iterable, Set, Tuple
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Dict, Iterable, NamedTuple, Set, Tuple
 
 from platformdirs import user_cache_dir
 
-from black.mode import Mode
-
 from _black_version import version as __version__
+from black.mode import Mode
 
+if sys.version_info >= (3, 11):
+    from typing import Self
+else:
+    from typing_extensions import Self
 
-# types
-Timestamp = float
-FileSize = int
-CacheInfo = Tuple[Timestamp, FileSize]
-Cache = Dict[str, CacheInfo]
 
+class FileData(NamedTuple):
+    st_mtime: float
+    st_size: int
+    hash: str
 
-CACHE_DIR = Path(user_cache_dir("black", version=__version__))
 
+def get_cache_dir() -> Path:
+    """Get the cache directory used by black.
 
-def read_cache(mode: Mode) -> Cache:
-    """Read the cache if it exists and is well formed.
+    Users can customize this directory on all systems using `BLACK_CACHE_DIR`
+    environment variable. By default, the cache directory is the user cache directory
+    under the black application.
 
-    If it is not well formed, the call to write_cache later should resolve the issue.
+    This result is immediately set to a constant `black.cache.CACHE_DIR` as to avoid
+    repeated calls.
     """
-    cache_file = get_cache_file(mode)
-    if not cache_file.exists():
-        return {}
+    # NOTE: Function mostly exists as a clean way to test getting the cache directory.
+    default_cache_dir = user_cache_dir("black")
+    cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir))
+    cache_dir = cache_dir / __version__
+    return cache_dir
 
-    with cache_file.open("rb") as fobj:
-        try:
-            cache: Cache = pickle.load(fobj)
-        except (pickle.UnpicklingError, ValueError):
-            return {}
 
-    return cache
+CACHE_DIR = get_cache_dir()
 
 
 def get_cache_file(mode: Mode) -> Path:
     return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle"
 
 
-def get_cache_info(path: Path) -> CacheInfo:
-    """Return the information used to check if a file is already formatted or not."""
-    stat = path.stat()
-    return stat.st_mtime, stat.st_size
-
-
-def filter_cached(cache: Cache, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path]]:
-    """Split an iterable of paths in `sources` into two sets.
-
-    The first contains paths of files that modified on disk or are not in the
-    cache. The other contains paths to non-modified files.
-    """
-    todo, done = set(), set()
-    for src in sources:
-        res_src = src.resolve()
-        if cache.get(str(res_src)) != get_cache_info(res_src):
-            todo.add(src)
-        else:
-            done.add(src)
-    return todo, done
-
-
-def write_cache(cache: Cache, sources: Iterable[Path], mode: Mode) -> None:
-    """Update the cache file."""
-    cache_file = get_cache_file(mode)
-    try:
-        CACHE_DIR.mkdir(parents=True, exist_ok=True)
-        new_cache = {
-            **cache,
-            **{str(src.resolve()): get_cache_info(src) for src in sources},
-        }
-        with tempfile.NamedTemporaryFile(dir=str(cache_file.parent), delete=False) as f:
-            pickle.dump(new_cache, f, protocol=4)
-        os.replace(f.name, cache_file)
-    except OSError:
-        pass
+@dataclass
+class Cache:
+    mode: Mode
+    cache_file: Path
+    file_data: Dict[str, FileData] = field(default_factory=dict)
+
+    @classmethod
+    def read(cls, mode: Mode) -> Self:
+        """Read the cache if it exists and is well formed.
+
+        If it is not well formed, the call to write later should
+        resolve the issue.
+        """
+        cache_file = get_cache_file(mode)
+        if not cache_file.exists():
+            return cls(mode, cache_file)
+
+        with cache_file.open("rb") as fobj:
+            try:
+                data: Dict[str, Tuple[float, int, str]] = pickle.load(fobj)
+                file_data = {k: FileData(*v) for k, v in data.items()}
+            except (pickle.UnpicklingError, ValueError, IndexError):
+                return cls(mode, cache_file)
+
+        return cls(mode, cache_file, file_data)
+
+    @staticmethod
+    def hash_digest(path: Path) -> str:
+        """Return hash digest for path."""
+
+        data = path.read_bytes()
+        return hashlib.sha256(data).hexdigest()
+
+    @staticmethod
+    def get_file_data(path: Path) -> FileData:
+        """Return file data for path."""
+
+        stat = path.stat()
+        hash = Cache.hash_digest(path)
+        return FileData(stat.st_mtime, stat.st_size, hash)
+
+    def is_changed(self, source: Path) -> bool:
+        """Check if source has changed compared to cached version."""
+        res_src = source.resolve()
+        old = self.file_data.get(str(res_src))
+        if old is None:
+            return True
+
+        st = res_src.stat()
+        if st.st_size != old.st_size:
+            return True
+        if int(st.st_mtime) != int(old.st_mtime):
+            new_hash = Cache.hash_digest(res_src)
+            if new_hash != old.hash:
+                return True
+        return False
+
+    def filtered_cached(self, sources: Iterable[Path]) -> Tuple[Set[Path], Set[Path]]:
+        """Split an iterable of paths in `sources` into two sets.
+
+        The first contains paths of files that modified on disk or are not in the
+        cache. The other contains paths to non-modified files.
+        """
+        changed: Set[Path] = set()
+        done: Set[Path] = set()
+        for src in sources:
+            if self.is_changed(src):
+                changed.add(src)
+            else:
+                done.add(src)
+        return changed, done
+
+    def write(self, sources: Iterable[Path]) -> None:
+        """Update the cache file data and write a new cache file."""
+        self.file_data.update(
+            **{str(src.resolve()): Cache.get_file_data(src) for src in sources}
+        )
+        try:
+            CACHE_DIR.mkdir(parents=True, exist_ok=True)
+            with tempfile.NamedTemporaryFile(
+                dir=str(self.cache_file.parent), delete=False
+            ) as f:
+                # We store raw tuples in the cache because pickling NamedTuples
+                # doesn't work with mypyc on Python 3.8, and because it's faster.
+                data: Dict[str, Tuple[float, int, str]] = {
+                    k: (*v,) for k, v in self.file_data.items()
+                }
+                pickle.dump(data, f, protocol=4)
+            os.replace(f.name, self.cache_file)
+        except OSError:
+            pass
index c7513c21ef5693cb5e5800eecbbc94a3fab88e7c..226968bff983f500d1f47ae60741e11e7cb10c80 100644 (file)
@@ -1,22 +1,29 @@
+import re
 from dataclasses import dataclass
 from functools import lru_cache
-import regex as re
-from typing import Iterator, List, Optional, Union
-
-from blib2to3.pytree import Node, Leaf
+from typing import Final, Iterator, List, Optional, Union
+
+from black.nodes import (
+    CLOSING_BRACKETS,
+    STANDALONE_COMMENT,
+    WHITESPACE,
+    container_of,
+    first_leaf_of,
+    preceding_leaf,
+    syms,
+)
 from blib2to3.pgen2 import token
-
-from black.nodes import first_leaf_column, preceding_leaf, container_of
-from black.nodes import STANDALONE_COMMENT, WHITESPACE
+from blib2to3.pytree import Leaf, Node
 
 # types
 LN = Union[Leaf, Node]
 
+FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"}
+FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"}
+FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP}
+FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"}
 
-FMT_OFF = {"# fmt: off", "# fmt:off", "# yapf: disable"}
-FMT_SKIP = {"# fmt: skip", "# fmt:skip"}
-FMT_PASS = {*FMT_OFF, *FMT_SKIP}
-FMT_ON = {"# fmt: on", "# fmt:on", "# yapf: enable"}
+COMMENT_EXCEPTIONS = " !:#'"
 
 
 @dataclass
@@ -100,7 +107,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]:
 def make_comment(content: str) -> str:
     """Return a consistently formatted comment from the given `content` string.
 
-    All comments (except for "##", "#!", "#:", '#'", "#%%") should have a single
+    All comments (except for "##", "#!", "#:", '#'") should have a single
     space between the hash sign and the content.
 
     If `content` didn't start with a hash sign, one is provided.
@@ -118,7 +125,7 @@ def make_comment(content: str) -> str:
         and not content.lstrip().startswith("type:")
     ):
         content = " " + content[1:]  # Replace NBSP by a simple space
-    if content and content[0] not in " !:#'%":
+    if content and content[0] not in COMMENT_EXCEPTIONS:
         content = " " + content
     return "#" + content
 
@@ -163,6 +170,11 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
                 first.prefix = prefix[comment.consumed :]
             if comment.value in FMT_SKIP:
                 first.prefix = ""
+                standalone_comment_prefix = prefix
+            else:
+                standalone_comment_prefix = (
+                    prefix[:previous_consumed] + "\n" * comment.newlines
+                )
             hidden_value = "".join(str(n) for n in ignored_nodes)
             if comment.value in FMT_OFF:
                 hidden_value = comment.value + "\n" + hidden_value
@@ -184,7 +196,8 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
                 Leaf(
                     STANDALONE_COMMENT,
                     hidden_value,
-                    prefix=prefix[:previous_consumed] + "\n" * comment.newlines,
+                    prefix=standalone_comment_prefix,
+                    fmt_pass_converted_first_leaf=first_leaf_of(first),
                 ),
             )
             return True
@@ -198,38 +211,87 @@ def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]:
     If comment is skip, returns leaf only.
     Stops at the end of the block.
     """
-    container: Optional[LN] = container_of(leaf)
     if comment.value in FMT_SKIP:
-        prev_sibling = leaf.prev_sibling
-        if comment.value in leaf.prefix and prev_sibling is not None:
-            leaf.prefix = leaf.prefix.replace(comment.value, "")
-            siblings = [prev_sibling]
-            while (
-                "\n" not in prev_sibling.prefix
-                and prev_sibling.prev_sibling is not None
-            ):
-                prev_sibling = prev_sibling.prev_sibling
-                siblings.insert(0, prev_sibling)
-            for sibling in siblings:
-                yield sibling
-        elif leaf.parent is not None:
-            yield leaf.parent
+        yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment)
         return
+    container: Optional[LN] = container_of(leaf)
     while container is not None and container.type != token.ENDMARKER:
         if is_fmt_on(container):
             return
 
         # fix for fmt: on in children
-        if contains_fmt_on_at_column(container, leaf.column):
-            for child in container.children:
-                if contains_fmt_on_at_column(child, leaf.column):
+        if children_contains_fmt_on(container):
+            for index, child in enumerate(container.children):
+                if isinstance(child, Leaf) and is_fmt_on(child):
+                    if child.type in CLOSING_BRACKETS:
+                        # This means `# fmt: on` is placed at a different bracket level
+                        # than `# fmt: off`. This is an invalid use, but as a courtesy,
+                        # we include this closing bracket in the ignored nodes.
+                        # The alternative is to fail the formatting.
+                        yield child
+                    return
+                if (
+                    child.type == token.INDENT
+                    and index < len(container.children) - 1
+                    and children_contains_fmt_on(container.children[index + 1])
+                ):
+                    # This means `# fmt: on` is placed right after an indentation
+                    # level, and we shouldn't swallow the previous INDENT token.
+                    return
+                if children_contains_fmt_on(child):
                     return
                 yield child
         else:
+            if container.type == token.DEDENT and container.next_sibling is None:
+                # This can happen when there is no matching `# fmt: on` comment at the
+                # same level as `# fmt: on`. We need to keep this DEDENT.
+                return
             yield container
             container = container.next_sibling
 
 
+def _generate_ignored_nodes_from_fmt_skip(
+    leaf: Leaf, comment: ProtoComment
+) -> Iterator[LN]:
+    """Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`."""
+    prev_sibling = leaf.prev_sibling
+    parent = leaf.parent
+    # Need to properly format the leaf prefix to compare it to comment.value,
+    # which is also formatted
+    comments = list_comments(leaf.prefix, is_endmarker=False)
+    if not comments or comment.value != comments[0].value:
+        return
+    if prev_sibling is not None:
+        leaf.prefix = ""
+        siblings = [prev_sibling]
+        while "\n" not in prev_sibling.prefix and prev_sibling.prev_sibling is not None:
+            prev_sibling = prev_sibling.prev_sibling
+            siblings.insert(0, prev_sibling)
+        yield from siblings
+    elif (
+        parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE
+    ):
+        # The `# fmt: skip` is on the colon line of the if/while/def/class/...
+        # statements. The ignored nodes should be previous siblings of the
+        # parent suite node.
+        leaf.prefix = ""
+        ignored_nodes: List[LN] = []
+        parent_sibling = parent.prev_sibling
+        while parent_sibling is not None and parent_sibling.type != syms.suite:
+            ignored_nodes.insert(0, parent_sibling)
+            parent_sibling = parent_sibling.prev_sibling
+        # Special case for `async_stmt` where the ASYNC token is on the
+        # grandparent node.
+        grandparent = parent.parent
+        if (
+            grandparent is not None
+            and grandparent.prev_sibling is not None
+            and grandparent.prev_sibling.type == token.ASYNC
+        ):
+            ignored_nodes.insert(0, grandparent.prev_sibling)
+        yield from iter(ignored_nodes)
+
+
 def is_fmt_on(container: LN) -> bool:
     """Determine whether formatting is switched on within a container.
     Determined by whether the last `# fmt:` comment is `on` or `off`.
@@ -243,17 +305,12 @@ def is_fmt_on(container: LN) -> bool:
     return fmt_on
 
 
-def contains_fmt_on_at_column(container: LN, column: int) -> bool:
-    """Determine if children at a given column have formatting switched on."""
+def children_contains_fmt_on(container: LN) -> bool:
+    """Determine if children have formatting switched on."""
     for child in container.children:
-        if (
-            isinstance(child, Node)
-            and first_leaf_column(child) == column
-            or isinstance(child, Leaf)
-            and child.column == column
-        ):
-            if is_fmt_on(child):
-                return True
+        leaf = first_leaf_of(child)
+        if leaf is not None and is_fmt_on(leaf):
+            return True
 
     return False
 
index 69d79f534e82fc32267e61a1a10e319ca51585bd..55c96b66c86e14fb4fb4b2a8f5aef54aa0e34dd3 100644 (file)
@@ -1,9 +1,27 @@
+"""
+Formatting many files at once via multiprocessing. Contains entrypoint and utilities.
+
+NOTE: this module is only imported if we need to format several files at once.
+"""
+
 import asyncio
 import logging
+import os
+import signal
 import sys
-from typing import Any, Iterable
+import traceback
+from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
+from multiprocessing import Manager
+from pathlib import Path
+from typing import Any, Iterable, Optional, Set
+
+from mypy_extensions import mypyc_attr
 
+from black import WriteBack, format_file_in_place
+from black.cache import Cache
+from black.mode import Mode
 from black.output import err
+from black.report import Changed, Report
 
 
 def maybe_install_uvloop() -> None:
@@ -11,7 +29,6 @@ def maybe_install_uvloop() -> None:
 
     This is called only from command-line entry points to avoid
     interfering with the parent process if Black is used as a library.
-
     """
     try:
         import uvloop
@@ -31,20 +48,14 @@ def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None:
 def shutdown(loop: asyncio.AbstractEventLoop) -> None:
     """Cancel all pending tasks on `loop`, wait for them, and close the loop."""
     try:
-        if sys.version_info[:2] >= (3, 7):
-            all_tasks = asyncio.all_tasks
-        else:
-            all_tasks = asyncio.Task.all_tasks
         # This part is borrowed from asyncio/runners.py in Python 3.7b2.
-        to_cancel = [task for task in all_tasks(loop) if not task.done()]
+        to_cancel = [task for task in asyncio.all_tasks(loop) if not task.done()]
         if not to_cancel:
             return
 
         for task in to_cancel:
             task.cancel()
-        loop.run_until_complete(
-            asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)
-        )
+        loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
     finally:
         # `concurrent.futures.Future` objects cannot be cancelled once they
         # are already running. There might be some when the `shutdown()` happened.
@@ -52,3 +63,128 @@ def shutdown(loop: asyncio.AbstractEventLoop) -> None:
         cf_logger = logging.getLogger("concurrent.futures")
         cf_logger.setLevel(logging.CRITICAL)
         loop.close()
+
+
+# diff-shades depends on being to monkeypatch this function to operate. I know it's
+# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
+@mypyc_attr(patchable=True)
+def reformat_many(
+    sources: Set[Path],
+    fast: bool,
+    write_back: WriteBack,
+    mode: Mode,
+    report: Report,
+    workers: Optional[int],
+) -> None:
+    """Reformat multiple files using a ProcessPoolExecutor."""
+    maybe_install_uvloop()
+
+    executor: Executor
+    if workers is None:
+        workers = int(os.environ.get("BLACK_NUM_WORKERS", 0))
+        workers = workers or os.cpu_count() or 1
+    if sys.platform == "win32":
+        # Work around https://bugs.python.org/issue26903
+        workers = min(workers, 60)
+    try:
+        executor = ProcessPoolExecutor(max_workers=workers)
+    except (ImportError, NotImplementedError, OSError):
+        # we arrive here if the underlying system does not support multi-processing
+        # like in AWS Lambda or Termux, in which case we gracefully fallback to
+        # a ThreadPoolExecutor with just a single worker (more workers would not do us
+        # any good due to the Global Interpreter Lock)
+        executor = ThreadPoolExecutor(max_workers=1)
+
+    loop = asyncio.new_event_loop()
+    asyncio.set_event_loop(loop)
+    try:
+        loop.run_until_complete(
+            schedule_formatting(
+                sources=sources,
+                fast=fast,
+                write_back=write_back,
+                mode=mode,
+                report=report,
+                loop=loop,
+                executor=executor,
+            )
+        )
+    finally:
+        try:
+            shutdown(loop)
+        finally:
+            asyncio.set_event_loop(None)
+        if executor is not None:
+            executor.shutdown()
+
+
+async def schedule_formatting(
+    sources: Set[Path],
+    fast: bool,
+    write_back: WriteBack,
+    mode: Mode,
+    report: "Report",
+    loop: asyncio.AbstractEventLoop,
+    executor: "Executor",
+) -> None:
+    """Run formatting of `sources` in parallel using the provided `executor`.
+
+    (Use ProcessPoolExecutors for actual parallelism.)
+
+    `write_back`, `fast`, and `mode` options are passed to
+    :func:`format_file_in_place`.
+    """
+    cache = Cache.read(mode)
+    if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
+        sources, cached = cache.filtered_cached(sources)
+        for src in sorted(cached):
+            report.done(src, Changed.CACHED)
+    if not sources:
+        return
+
+    cancelled = []
+    sources_to_cache = []
+    lock = None
+    if write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
+        # For diff output, we need locks to ensure we don't interleave output
+        # from different processes.
+        manager = Manager()
+        lock = manager.Lock()
+    tasks = {
+        asyncio.ensure_future(
+            loop.run_in_executor(
+                executor, format_file_in_place, src, fast, mode, write_back, lock
+            )
+        ): src
+        for src in sorted(sources)
+    }
+    pending = tasks.keys()
+    try:
+        loop.add_signal_handler(signal.SIGINT, cancel, pending)
+        loop.add_signal_handler(signal.SIGTERM, cancel, pending)
+    except NotImplementedError:
+        # There are no good alternatives for these on Windows.
+        pass
+    while pending:
+        done, _ = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
+        for task in done:
+            src = tasks.pop(task)
+            if task.cancelled():
+                cancelled.append(task)
+            elif exc := task.exception():
+                if report.verbose:
+                    traceback.print_exception(type(exc), exc, exc.__traceback__)
+                report.failed(src, str(exc))
+            else:
+                changed = Changed.YES if task.result() else Changed.NO
+                # If the file was written back or was successfully checked as
+                # well-formatted, store this information in the cache.
+                if write_back is WriteBack.YES or (
+                    write_back is WriteBack.CHECK and changed is Changed.NO
+                ):
+                    sources_to_cache.append(src)
+                report.done(src, changed)
+    if cancelled:
+        await asyncio.gather(*cancelled, return_exceptions=True)
+    if sources_to_cache:
+        cache.write(sources_to_cache)
index dbb4826be0e9f9d4e1694dc32fe1b385240fbda5..ee466679c7074caf936b1e2707b82468e8d83432 100644 (file)
@@ -1,4 +1,4 @@
 DEFAULT_LINE_LENGTH = 88
-DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|venv|\.svn|_build|buck-out|build|dist)/"  # noqa: B950
+DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.ipynb_checkpoints|\.mypy_cache|\.nox|\.pytest_cache|\.ruff_cache|\.tox|\.svn|\.venv|\.vscode|__pypackages__|_build|buck-out|build|dist|venv)/"  # noqa: B950
 DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$"
 STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__"
index 5143076ab35ff1c35a7b8a823e0c672078411b1f..cebc48765babd1ea989f64cf88cbe094f8370a60 100644 (file)
@@ -1,12 +1,11 @@
-from dataclasses import dataclass
-from typing import Iterator, TypeVar, Union
-
-from blib2to3.pytree import Node, Leaf, type_repr
-from blib2to3.pgen2 import token
+from dataclasses import dataclass, field
+from typing import Any, Iterator, List, TypeVar, Union
 
 from black.nodes import Visitor
 from black.output import out
 from black.parsing import lib2to3_parse
+from blib2to3.pgen2 import token
+from blib2to3.pytree import Leaf, Node, type_repr
 
 LN = Union[Leaf, Node]
 T = TypeVar("T")
@@ -15,26 +14,33 @@ T = TypeVar("T")
 @dataclass
 class DebugVisitor(Visitor[T]):
     tree_depth: int = 0
+    list_output: List[str] = field(default_factory=list)
+    print_output: bool = True
+
+    def out(self, message: str, *args: Any, **kwargs: Any) -> None:
+        self.list_output.append(message)
+        if self.print_output:
+            out(message, *args, **kwargs)
 
     def visit_default(self, node: LN) -> Iterator[T]:
         indent = " " * (2 * self.tree_depth)
         if isinstance(node, Node):
             _type = type_repr(node.type)
-            out(f"{indent}{_type}", fg="yellow")
+            self.out(f"{indent}{_type}", fg="yellow")
             self.tree_depth += 1
             for child in node.children:
                 yield from self.visit(child)
 
             self.tree_depth -= 1
-            out(f"{indent}/{_type}", fg="yellow", bold=False)
+            self.out(f"{indent}/{_type}", fg="yellow", bold=False)
         else:
             _type = token.tok_name.get(node.type, str(node.type))
-            out(f"{indent}{_type}", fg="blue", nl=False)
+            self.out(f"{indent}{_type}", fg="blue", nl=False)
             if node.prefix:
                 # We don't have to handle prefixes for `Node` objects since
                 # that delegates to the first child anyway.
-                out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
-            out(f" {node.value!r}", fg="blue", bold=False)
+                self.out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
+            self.out(f" {node.value!r}", fg="blue", bold=False)
 
     @classmethod
     def show(cls, code: Union[str, Leaf, Node]) -> None:
index 4d7b47aaa9fcbb4b7abe96ded72c9811d0155402..362898dc0fd71bdc1e19d5c589837c2d0c2a0c96 100644 (file)
@@ -1,9 +1,10 @@
-from functools import lru_cache
 import io
 import os
-from pathlib import Path
 import sys
+from functools import lru_cache
+from pathlib import Path
 from typing import (
+    TYPE_CHECKING,
     Any,
     Dict,
     Iterable,
@@ -14,23 +15,37 @@ from typing import (
     Sequence,
     Tuple,
     Union,
-    TYPE_CHECKING,
 )
 
+from mypy_extensions import mypyc_attr
+from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
+from packaging.version import InvalidVersion, Version
 from pathspec import PathSpec
 from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
-import tomli
 
+if sys.version_info >= (3, 11):
+    try:
+        import tomllib
+    except ImportError:
+        # Help users on older alphas
+        if not TYPE_CHECKING:
+            import tomli as tomllib
+else:
+    import tomli as tomllib
+
+from black.handle_ipynb_magics import jupyter_dependencies_are_installed
+from black.mode import TargetVersion
 from black.output import err
 from black.report import Report
-from black.handle_ipynb_magics import jupyter_dependencies_are_installed
 
 if TYPE_CHECKING:
     import colorama  # noqa: F401
 
 
-@lru_cache()
-def find_project_root(srcs: Sequence[str]) -> Path:
+@lru_cache
+def find_project_root(
+    srcs: Sequence[str], stdin_filename: Optional[str] = None
+) -> Tuple[Path, str]:
     """Return a directory containing .git, .hg, or pyproject.toml.
 
     That directory will be a common parent of all files and directories
@@ -38,7 +53,13 @@ def find_project_root(srcs: Sequence[str]) -> Path:
 
     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.
+
+    Returns a two-tuple with the first element as the project root path and
+    the second element as a string describing the method by which the
+    project root was discovered.
     """
+    if stdin_filename is not None:
+        srcs = tuple(stdin_filename if s == "-" else s for s in srcs)
     if not srcs:
         srcs = [str(Path.cwd().resolve())]
 
@@ -57,20 +78,22 @@ def find_project_root(srcs: Sequence[str]) -> Path:
 
     for directory in (common_base, *common_base.parents):
         if (directory / ".git").exists():
-            return directory
+            return directory, ".git directory"
 
         if (directory / ".hg").is_dir():
-            return directory
+            return directory, ".hg directory"
 
         if (directory / "pyproject.toml").is_file():
-            return directory
+            return directory, "pyproject.toml"
 
-    return directory
+    return directory, "file system root"
 
 
-def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
+def find_pyproject_toml(
+    path_search_start: Tuple[str, ...], stdin_filename: Optional[str] = None
+) -> Optional[str]:
     """Find the absolute filepath to a pyproject.toml if it exists"""
-    path_project_root = find_project_root(path_search_start)
+    path_project_root, _ = find_project_root(path_search_start, stdin_filename)
     path_pyproject_toml = path_project_root / "pyproject.toml"
     if path_pyproject_toml.is_file():
         return str(path_pyproject_toml)
@@ -82,29 +105,123 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
             if path_user_pyproject_toml.is_file()
             else None
         )
-    except PermissionError as e:
+    except (PermissionError, RuntimeError) as e:
         # We do not have access to the user-level config directory, so ignore it.
         err(f"Ignoring user configuration directory due to {e!r}")
         return None
 
 
+@mypyc_attr(patchable=True)
 def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
-    """Parse a pyproject toml file, pulling out relevant parts for Black
+    """Parse a pyproject toml file, pulling out relevant parts for Black.
 
-    If parsing fails, will raise a tomli.TOMLDecodeError
+    If parsing fails, will raise a tomllib.TOMLDecodeError.
     """
-    with open(path_config, encoding="utf8") as f:
-        pyproject_toml = tomli.load(f)  # type: ignore  # due to deprecated API usage
-    config = pyproject_toml.get("tool", {}).get("black", {})
-    return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
+    with open(path_config, "rb") as f:
+        pyproject_toml = tomllib.load(f)
+    config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("black", {})
+    config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
 
+    if "target_version" not in config:
+        inferred_target_version = infer_target_version(pyproject_toml)
+        if inferred_target_version is not None:
+            config["target_version"] = [v.name.lower() for v in inferred_target_version]
 
-@lru_cache()
+    return config
+
+
+def infer_target_version(
+    pyproject_toml: Dict[str, Any]
+) -> Optional[List[TargetVersion]]:
+    """Infer Black's target version from the project metadata in pyproject.toml.
+
+    Supports the PyPA standard format (PEP 621):
+    https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#requires-python
+
+    If the target version cannot be inferred, returns None.
+    """
+    project_metadata = pyproject_toml.get("project", {})
+    requires_python = project_metadata.get("requires-python", None)
+    if requires_python is not None:
+        try:
+            return parse_req_python_version(requires_python)
+        except InvalidVersion:
+            pass
+        try:
+            return parse_req_python_specifier(requires_python)
+        except (InvalidSpecifier, InvalidVersion):
+            pass
+
+    return None
+
+
+def parse_req_python_version(requires_python: str) -> Optional[List[TargetVersion]]:
+    """Parse a version string (i.e. ``"3.7"``) to a list of TargetVersion.
+
+    If parsing fails, will raise a packaging.version.InvalidVersion error.
+    If the parsed version cannot be mapped to a valid TargetVersion, returns None.
+    """
+    version = Version(requires_python)
+    if version.release[0] != 3:
+        return None
+    try:
+        return [TargetVersion(version.release[1])]
+    except (IndexError, ValueError):
+        return None
+
+
+def parse_req_python_specifier(requires_python: str) -> Optional[List[TargetVersion]]:
+    """Parse a specifier string (i.e. ``">=3.7,<3.10"``) to a list of TargetVersion.
+
+    If parsing fails, will raise a packaging.specifiers.InvalidSpecifier error.
+    If the parsed specifier cannot be mapped to a valid TargetVersion, returns None.
+    """
+    specifier_set = strip_specifier_set(SpecifierSet(requires_python))
+    if not specifier_set:
+        return None
+
+    target_version_map = {f"3.{v.value}": v for v in TargetVersion}
+    compatible_versions: List[str] = list(specifier_set.filter(target_version_map))
+    if compatible_versions:
+        return [target_version_map[v] for v in compatible_versions]
+    return None
+
+
+def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet:
+    """Strip minor versions for some specifiers in the specifier set.
+
+    For background on version specifiers, see PEP 440:
+    https://peps.python.org/pep-0440/#version-specifiers
+    """
+    specifiers = []
+    for s in specifier_set:
+        if "*" in str(s):
+            specifiers.append(s)
+        elif s.operator in ["~=", "==", ">=", "==="]:
+            version = Version(s.version)
+            stripped = Specifier(f"{s.operator}{version.major}.{version.minor}")
+            specifiers.append(stripped)
+        elif s.operator == ">":
+            version = Version(s.version)
+            if len(version.release) > 2:
+                s = Specifier(f">={version.major}.{version.minor}")
+            specifiers.append(s)
+        else:
+            specifiers.append(s)
+
+    return SpecifierSet(",".join(str(s) for s in specifiers))
+
+
+@lru_cache
 def find_user_pyproject_toml() -> Path:
     r"""Return the path to the top-level user configuration for black.
 
     This looks for ~\.black on Windows and ~/.config/black on Linux and other
     Unix systems.
+
+    May raise:
+    - RuntimeError: if the current user has no homedir
+    - PermissionError: if the current process cannot access the user's homedir
     """
     if sys.platform == "win32":
         # Windows
@@ -115,7 +232,7 @@ def find_user_pyproject_toml() -> Path:
     return user_config_path.resolve()
 
 
-@lru_cache()
+@lru_cache
 def get_gitignore(root: Path) -> PathSpec:
     """Return a PathSpec matching gitignore content if present."""
     gitignore = root / ".gitignore"
@@ -131,7 +248,9 @@ def get_gitignore(root: Path) -> PathSpec:
 
 
 def normalize_path_maybe_ignore(
-    path: Path, root: Path, report: Report
+    path: Path,
+    root: Path,
+    report: Optional[Report] = None,
 ) -> Optional[str]:
     """Normalize `path`. May return `None` if `path` was ignored.
 
@@ -139,19 +258,44 @@ def normalize_path_maybe_ignore(
     """
     try:
         abspath = path if path.is_absolute() else Path.cwd() / path
-        normalized_path = abspath.resolve().relative_to(root).as_posix()
+        normalized_path = abspath.resolve()
+        try:
+            root_relative_path = normalized_path.relative_to(root).as_posix()
+        except ValueError:
+            if report:
+                report.path_ignored(
+                    path, f"is a symbolic link that points outside {root}"
+                )
+            return None
+
     except OSError as e:
-        report.path_ignored(path, f"cannot be read because {e}")
+        if report:
+            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
+    return root_relative_path
 
-        raise
 
-    return normalized_path
+def _path_is_ignored(
+    root_relative_path: str,
+    root: Path,
+    gitignore_dict: Dict[Path, PathSpec],
+    report: Report,
+) -> bool:
+    path = root / root_relative_path
+    # Note that this logic is sensitive to the ordering of gitignore_dict. Callers must
+    # ensure that gitignore_dict is ordered from least specific to most specific.
+    for gitignore_path, pattern in gitignore_dict.items():
+        try:
+            relative_path = path.relative_to(gitignore_path).as_posix()
+        except ValueError:
+            break
+        if pattern.match_file(relative_path):
+            report.path_ignored(
+                path.relative_to(root), "matches a .gitignore file content"
+            )
+            return True
+    return False
 
 
 def path_is_excluded(
@@ -170,7 +314,7 @@ def gen_python_files(
     extend_exclude: Optional[Pattern[str]],
     force_exclude: Optional[Pattern[str]],
     report: Report,
-    gitignore: Optional[PathSpec],
+    gitignore_dict: Optional[Dict[Path, PathSpec]],
     *,
     verbose: bool,
     quiet: bool,
@@ -183,39 +327,50 @@ def gen_python_files(
 
     `report` is where output about exclusions goes.
     """
+
     assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
     for child in paths:
-        normalized_path = normalize_path_maybe_ignore(child, root, report)
-        if normalized_path is None:
-            continue
+        root_relative_path = child.absolute().relative_to(root).as_posix()
 
         # First ignore files matching .gitignore, if passed
-        if gitignore is not None and gitignore.match_file(normalized_path):
-            report.path_ignored(child, "matches the .gitignore file content")
+        if gitignore_dict and _path_is_ignored(
+            root_relative_path, root, gitignore_dict, report
+        ):
             continue
 
         # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options.
-        normalized_path = "/" + normalized_path
+        root_relative_path = "/" + root_relative_path
         if child.is_dir():
-            normalized_path += "/"
+            root_relative_path += "/"
 
-        if path_is_excluded(normalized_path, exclude):
+        if path_is_excluded(root_relative_path, exclude):
             report.path_ignored(child, "matches the --exclude regular expression")
             continue
 
-        if path_is_excluded(normalized_path, extend_exclude):
+        if path_is_excluded(root_relative_path, extend_exclude):
             report.path_ignored(
                 child, "matches the --extend-exclude regular expression"
             )
             continue
 
-        if path_is_excluded(normalized_path, force_exclude):
+        if path_is_excluded(root_relative_path, force_exclude):
             report.path_ignored(child, "matches the --force-exclude regular expression")
             continue
 
+        normalized_path = normalize_path_maybe_ignore(child, root, report)
+        if normalized_path is None:
+            continue
+
         if child.is_dir():
             # If gitignore is None, gitignore usage is disabled, while a Falsey
             # gitignore is when the directory doesn't have a .gitignore file.
+            if gitignore_dict is not None:
+                new_gitignore_dict = {
+                    **gitignore_dict,
+                    root / child: get_gitignore(child),
+                }
+            else:
+                new_gitignore_dict = None
             yield from gen_python_files(
                 child.iterdir(),
                 root,
@@ -224,14 +379,14 @@ def gen_python_files(
                 extend_exclude,
                 force_exclude,
                 report,
-                gitignore + get_gitignore(child) if gitignore is not None else None,
+                new_gitignore_dict,
                 verbose=verbose,
                 quiet=quiet,
             )
 
         elif child.is_file():
             if child.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
-                verbose=verbose, quiet=quiet
+                warn=verbose or not quiet
             ):
                 continue
             include_match = include.search(normalized_path) if include else True
index 63c8aafe35b70e4d16b7d59f349feeb6f392601a..55ef2267df829f3b38de9ff5ce02f0d0a73edef3 100644 (file)
@@ -1,22 +1,21 @@
 """Functions to process IPython magics with."""
 
-from functools import lru_cache
-import dataclasses
 import ast
-from typing import Dict, List, Tuple, Optional
-
+import collections
+import dataclasses
 import secrets
 import sys
-import collections
+from functools import lru_cache
+from importlib.util import find_spec
+from typing import Dict, List, Optional, Tuple
 
 if sys.version_info >= (3, 10):
     from typing import TypeGuard
 else:
     from typing_extensions import TypeGuard
 
-from black.report import NothingChanged
 from black.output import out
-
+from black.report import NothingChanged
 
 TRANSFORMED_MAGICS = frozenset(
     (
@@ -37,20 +36,15 @@ TOKENS_TO_IGNORE = frozenset(
         "ESCAPED_NL",
     )
 )
-NON_PYTHON_CELL_MAGICS = frozenset(
+PYTHON_CELL_MAGICS = frozenset(
     (
-        "%%bash",
-        "%%html",
-        "%%javascript",
-        "%%js",
-        "%%latex",
-        "%%markdown",
-        "%%perl",
-        "%%ruby",
-        "%%script",
-        "%%sh",
-        "%%svg",
-        "%%writefile",
+        "capture",
+        "prun",
+        "pypy",
+        "python",
+        "python3",
+        "time",
+        "timeit",
     )
 )
 TOKEN_HEX = secrets.token_hex
@@ -62,21 +56,18 @@ class Replacement:
     src: str
 
 
-@lru_cache()
-def jupyter_dependencies_are_installed(*, verbose: bool, quiet: bool) -> bool:
-    try:
-        import IPython  # noqa:F401
-        import tokenize_rt  # noqa:F401
-    except ModuleNotFoundError:
-        if verbose or not quiet:
-            msg = (
-                "Skipping .ipynb files as Jupyter dependencies are not installed.\n"
-                "You can fix this by running ``pip install black[jupyter]``"
-            )
-            out(msg)
-        return False
-    else:
-        return True
+@lru_cache
+def jupyter_dependencies_are_installed(*, warn: bool) -> bool:
+    installed = (
+        find_spec("tokenize_rt") is not None and find_spec("IPython") is not None
+    )
+    if not installed and warn:
+        msg = (
+            "Skipping .ipynb files as Jupyter dependencies are not installed.\n"
+            'You can fix this by running ``pip install "black[jupyter]"``'
+        )
+        out(msg)
+    return installed
 
 
 def remove_trailing_semicolon(src: str) -> Tuple[str, bool]:
@@ -95,11 +86,7 @@ def remove_trailing_semicolon(src: str) -> Tuple[str, bool]:
     Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses
     ``tokenize_rt`` so that round-tripping works fine.
     """
-    from tokenize_rt import (
-        src_to_tokens,
-        tokens_to_src,
-        reversed_enumerate,
-    )
+    from tokenize_rt import reversed_enumerate, src_to_tokens, tokens_to_src
 
     tokens = src_to_tokens(src)
     trailing_semicolon = False
@@ -123,7 +110,7 @@ def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str:
     """
     if not has_trailing_semicolon:
         return src
-    from tokenize_rt import src_to_tokens, tokens_to_src, reversed_enumerate
+    from tokenize_rt import reversed_enumerate, src_to_tokens, tokens_to_src
 
     tokens = src_to_tokens(src)
     for idx, token in reversed_enumerate(tokens):
@@ -230,10 +217,9 @@ def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]:
     cell_magic_finder.visit(tree)
     if cell_magic_finder.cell_magic is None:
         return src, replacements
-    if cell_magic_finder.cell_magic.header.split()[0] in NON_PYTHON_CELL_MAGICS:
-        raise NothingChanged
-    mask = get_token(src, cell_magic_finder.cell_magic.header)
-    replacements.append(Replacement(mask=mask, src=cell_magic_finder.cell_magic.header))
+    header = cell_magic_finder.cell_magic.header
+    mask = get_token(src, header)
+    replacements.append(Replacement(mask=mask, src=header))
     return f"{mask}\n{cell_magic_finder.cell_magic.body}", replacements
 
 
@@ -311,13 +297,28 @@ def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]:
     )
 
 
+def _get_str_args(args: List[ast.expr]) -> List[str]:
+    str_args = []
+    for arg in args:
+        assert isinstance(arg, ast.Str)
+        str_args.append(arg.s)
+    return str_args
+
+
 @dataclasses.dataclass(frozen=True)
 class CellMagic:
-    header: str
+    name: str
+    params: Optional[str]
     body: str
 
+    @property
+    def header(self) -> str:
+        if self.params:
+            return f"%%{self.name} {self.params}"
+        return f"%%{self.name}"
+
 
-@dataclasses.dataclass
+# ast.NodeVisitor + dataclass = breakage under mypyc.
 class CellMagicFinder(ast.NodeVisitor):
     """Find cell magics.
 
@@ -327,7 +328,8 @@ class CellMagicFinder(ast.NodeVisitor):
 
     For example,
 
-        %%time\nfoo()
+        %%time\n
+        foo()
 
     would have been transformed to
 
@@ -336,7 +338,8 @@ class CellMagicFinder(ast.NodeVisitor):
     and we look for instances of the latter.
     """
 
-    cell_magic: Optional[CellMagic] = None
+    def __init__(self, cell_magic: Optional[CellMagic] = None) -> None:
+        self.cell_magic = cell_magic
 
     def visit_Expr(self, node: ast.Expr) -> None:
         """Find cell magic, extract header and body."""
@@ -345,14 +348,8 @@ class CellMagicFinder(ast.NodeVisitor):
             and _is_ipython_magic(node.value.func)
             and node.value.func.attr == "run_cell_magic"
         ):
-            args = []
-            for arg in node.value.args:
-                assert isinstance(arg, ast.Str)
-                args.append(arg.s)
-            header = f"%%{args[0]}"
-            if args[1]:
-                header += f" {args[1]}"
-            self.cell_magic = CellMagic(header=header, body=args[2])
+            args = _get_str_args(node.value.args)
+            self.cell_magic = CellMagic(name=args[0], params=args[1], body=args[2])
         self.generic_visit(node)
 
 
@@ -362,7 +359,8 @@ class OffsetAndMagic:
     magic: str
 
 
-@dataclasses.dataclass
+# Unsurprisingly, subclassing ast.NodeVisitor means we can't use dataclasses here
+# as mypyc will generate broken code.
 class MagicFinder(ast.NodeVisitor):
     """Visit cell to look for get_ipython calls.
 
@@ -382,9 +380,8 @@ class MagicFinder(ast.NodeVisitor):
     types of magics).
     """
 
-    magics: Dict[int, List[OffsetAndMagic]] = dataclasses.field(
-        default_factory=lambda: collections.defaultdict(list)
-    )
+    def __init__(self) -> None:
+        self.magics: Dict[int, List[OffsetAndMagic]] = collections.defaultdict(list)
 
     def visit_Assign(self, node: ast.Assign) -> None:
         """Look for system assign magics.
@@ -392,24 +389,28 @@ class MagicFinder(ast.NodeVisitor):
         For example,
 
             black_version = !black --version
+            env = %env var
 
-        would have been transformed to
+        would have been (respectively) transformed to
 
             black_version = get_ipython().getoutput('black --version')
+            env = get_ipython().run_line_magic('env', 'var')
 
-        and we look for instances of the latter.
+        and we look for instances of any of the latter.
         """
-        if (
-            isinstance(node.value, ast.Call)
-            and _is_ipython_magic(node.value.func)
-            and node.value.func.attr == "getoutput"
-        ):
-            args = []
-            for arg in node.value.args:
-                assert isinstance(arg, ast.Str)
-                args.append(arg.s)
-            assert args
-            src = f"!{args[0]}"
+        if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func):
+            args = _get_str_args(node.value.args)
+            if node.value.func.attr == "getoutput":
+                src = f"!{args[0]}"
+            elif node.value.func.attr == "run_line_magic":
+                src = f"%{args[0]}"
+                if args[1]:
+                    src += f" {args[1]}"
+            else:
+                raise AssertionError(
+                    f"Unexpected IPython magic {node.value.func.attr!r} found. "
+                    "Please report a bug on https://github.com/psf/black/issues."
+                ) from None
             self.magics[node.value.lineno].append(
                 OffsetAndMagic(node.value.col_offset, src)
             )
@@ -435,11 +436,7 @@ class MagicFinder(ast.NodeVisitor):
         and we look for instances of any of the latter.
         """
         if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func):
-            args = []
-            for arg in node.value.args:
-                assert isinstance(arg, ast.Str)
-                args.append(arg.s)
-            assert args
+            args = _get_str_args(node.value.args)
             if node.value.func.attr == "run_line_magic":
                 if args[0] == "pinfo":
                     src = f"?{args[1]}"
@@ -448,7 +445,6 @@ class MagicFinder(ast.NodeVisitor):
                 else:
                     src = f"%{args[0]}"
                     if args[1]:
-                        assert src is not None
                         src += f" {args[1]}"
             elif node.value.func.attr == "system":
                 src = f"!{args[0]}"
index eb53fa0ac56e0aa2520d3b27248d3e4123c7c7ef..2bfe587fa0ec5120287d133c7371ee85716d5ec7 100644 (file)
@@ -1,35 +1,81 @@
 """
 Generating lines of code.
 """
-from functools import partial, wraps
+
 import sys
-from typing import Collection, Iterator, List, Optional, Set, Union
-
-from dataclasses import dataclass, field
-
-from black.nodes import WHITESPACE, RARROW, STATEMENT, STANDALONE_COMMENT
-from black.nodes import ASSIGNMENTS, OPENING_BRACKETS, CLOSING_BRACKETS
-from black.nodes import Visitor, syms, first_child_is_arith, ensure_visible
-from black.nodes import is_docstring, is_empty_tuple, is_one_tuple, is_one_tuple_between
-from black.nodes import is_walrus_assignment, is_yield, is_vararg, is_multiline_string
-from black.nodes import is_stub_suite, is_stub_body, is_atom_with_invisible_parens
-from black.nodes import wrap_in_parentheses
-from black.brackets import max_delimiter_priority_in_atom
-from black.brackets import DOT_PRIORITY, COMMA_PRIORITY
-from black.lines import Line, line_to_string, is_line_short_enough
-from black.lines import can_omit_invisible_parens, can_be_split, append_leaves
-from black.comments import generate_comments, list_comments, FMT_OFF
+from dataclasses import replace
+from enum import Enum, auto
+from functools import partial, wraps
+from typing import Collection, Iterator, List, Optional, Set, Union, cast
+
+from black.brackets import (
+    COMMA_PRIORITY,
+    DOT_PRIORITY,
+    get_leaves_inside_matching_brackets,
+    max_delimiter_priority_in_atom,
+)
+from black.comments import FMT_OFF, generate_comments, list_comments
+from black.lines import (
+    Line,
+    RHSResult,
+    append_leaves,
+    can_be_split,
+    can_omit_invisible_parens,
+    is_line_short_enough,
+    line_to_string,
+)
+from black.mode import Feature, Mode, Preview
+from black.nodes import (
+    ASSIGNMENTS,
+    BRACKETS,
+    CLOSING_BRACKETS,
+    OPENING_BRACKETS,
+    RARROW,
+    STANDALONE_COMMENT,
+    STATEMENT,
+    WHITESPACE,
+    Visitor,
+    ensure_visible,
+    is_arith_like,
+    is_async_stmt_or_funcdef,
+    is_atom_with_invisible_parens,
+    is_docstring,
+    is_empty_tuple,
+    is_lpar_token,
+    is_multiline_string,
+    is_name_token,
+    is_one_sequence_between,
+    is_one_tuple,
+    is_rpar_token,
+    is_stub_body,
+    is_stub_suite,
+    is_tuple_containing_walrus,
+    is_type_ignore_comment_string,
+    is_vararg,
+    is_walrus_assignment,
+    is_yield,
+    syms,
+    wrap_in_parentheses,
+)
 from black.numerics import normalize_numeric_literal
-from black.strings import get_string_prefix, fix_docstring
-from black.strings import normalize_string_prefix, normalize_string_quotes
-from black.trans import Transformer, CannotTransform, StringMerger
-from black.trans import StringSplitter, StringParenWrapper, StringParenStripper
-from black.mode import Mode
-from black.mode import Feature
-
-from blib2to3.pytree import Node, Leaf
+from black.strings import (
+    fix_docstring,
+    get_string_prefix,
+    normalize_string_prefix,
+    normalize_string_quotes,
+    normalize_unicode_escape_sequences,
+)
+from black.trans import (
+    CannotTransform,
+    StringMerger,
+    StringParenStripper,
+    StringParenWrapper,
+    StringSplitter,
+    Transformer,
+    hug_power_op,
+)
 from blib2to3.pgen2 import token
-
+from blib2to3.pytree import Leaf, Node
 
 # types
 LeafID = int
@@ -40,7 +86,8 @@ class CannotSplit(CannotTransform):
     """A readable split that fits the allotted line length is impossible."""
 
 
-@dataclass
+# This isn't a dataclass because @dataclass + Generic breaks mypyc.
+# See also https://github.com/mypyc/mypyc/issues/827.
 class LineGenerator(Visitor[Line]):
     """Generates reformatted Line objects.  Empty lines are not emitted.
 
@@ -48,9 +95,11 @@ class LineGenerator(Visitor[Line]):
     in ways that will no longer stringify to valid Python code on the tree.
     """
 
-    mode: Mode
-    remove_u_prefix: bool = False
-    current_line: Line = field(init=False)
+    def __init__(self, mode: Mode, features: Collection[Feature]) -> None:
+        self.mode = mode
+        self.features = features
+        self.current_line: Line
+        self.__post_init__()
 
     def line(self, indent: int = 0) -> Iterator[Line]:
         """Generate a line.
@@ -64,6 +113,17 @@ class LineGenerator(Visitor[Line]):
             self.current_line.depth += indent
             return  # Line is empty, don't emit. Creating a new one unnecessary.
 
+        if (
+            Preview.improved_async_statements_handling in self.mode
+            and len(self.current_line.leaves) == 1
+            and is_async_stmt_or_funcdef(self.current_line.leaves[0])
+        ):
+            # Special case for async def/for/with statements. `visit_async_stmt`
+            # adds an `ASYNC` leaf then visits the child def/for/with statement
+            # nodes. Line yields from those nodes shouldn't treat the former
+            # `ASYNC` leaf as a complete line.
+            return
+
         complete_line = self.current_line
         self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent)
         yield complete_line
@@ -90,9 +150,7 @@ class LineGenerator(Visitor[Line]):
 
             normalize_prefix(node, inside_brackets=any_open_brackets)
             if self.mode.string_normalization and node.type == token.STRING:
-                node.value = normalize_string_prefix(
-                    node.value, remove_u_prefix=self.remove_u_prefix
-                )
+                node.value = normalize_string_prefix(node.value)
                 node.value = normalize_string_quotes(node.value)
             if node.type == token.NUMBER:
                 normalize_numeric_literal(node)
@@ -100,6 +158,22 @@ class LineGenerator(Visitor[Line]):
                 self.current_line.append(node)
         yield from super().visit_default(node)
 
+    def visit_test(self, node: Node) -> Iterator[Line]:
+        """Visit an `x if y else z` test"""
+
+        if Preview.parenthesize_conditional_expressions in self.mode:
+            already_parenthesized = (
+                node.prev_sibling and node.prev_sibling.type == token.LPAR
+            )
+
+            if not already_parenthesized:
+                lpar = Leaf(token.LPAR, "")
+                rpar = Leaf(token.RPAR, "")
+                node.insert_child(0, lpar)
+                node.append_child(rpar)
+
+        yield from self.visit_default(node)
+
     def visit_INDENT(self, node: Leaf) -> Iterator[Line]:
         """Increase indentation level, maybe yield a line."""
         # In blib2to3 INDENT never holds comments.
@@ -126,7 +200,7 @@ class LineGenerator(Visitor[Line]):
         """Visit a statement.
 
         This implementation is shared for `if`, `while`, `for`, `try`, `except`,
-        `def`, `with`, `class`, `assert` and assignments.
+        `def`, `with`, `class`, `assert`, and assignments.
 
         The relevant Python language `keywords` for a given statement will be
         NAME leaves within it. This methods puts those on a separate line.
@@ -134,27 +208,100 @@ class LineGenerator(Visitor[Line]):
         `parens` holds a set of string leaf values immediately after which
         invisible parens should be put.
         """
-        normalize_invisible_parens(node, parens_after=parens)
+        normalize_invisible_parens(
+            node, parens_after=parens, mode=self.mode, features=self.features
+        )
         for child in node.children:
-            if child.type == token.NAME and child.value in keywords:  # type: ignore
+            if is_name_token(child) and child.value in keywords:
                 yield from self.line()
 
             yield from self.visit(child)
 
+    def visit_typeparams(self, node: Node) -> Iterator[Line]:
+        yield from self.visit_default(node)
+        node.children[0].prefix = ""
+
+    def visit_typevartuple(self, node: Node) -> Iterator[Line]:
+        yield from self.visit_default(node)
+        node.children[1].prefix = ""
+
+    def visit_paramspec(self, node: Node) -> Iterator[Line]:
+        yield from self.visit_default(node)
+        node.children[1].prefix = ""
+
+    def visit_dictsetmaker(self, node: Node) -> Iterator[Line]:
+        if Preview.wrap_long_dict_values_in_parens in self.mode:
+            for i, child in enumerate(node.children):
+                if i == 0:
+                    continue
+                if node.children[i - 1].type == token.COLON:
+                    if child.type == syms.atom and child.children[0].type == token.LPAR:
+                        if maybe_make_parens_invisible_in_atom(
+                            child,
+                            parent=node,
+                            remove_brackets_around_comma=False,
+                        ):
+                            wrap_in_parentheses(node, child, visible=False)
+                    else:
+                        wrap_in_parentheses(node, child, visible=False)
+        yield from self.visit_default(node)
+
+    def visit_funcdef(self, node: Node) -> Iterator[Line]:
+        """Visit function definition."""
+        yield from self.line()
+
+        # Remove redundant brackets around return type annotation.
+        is_return_annotation = False
+        for child in node.children:
+            if child.type == token.RARROW:
+                is_return_annotation = True
+            elif is_return_annotation:
+                if child.type == syms.atom and child.children[0].type == token.LPAR:
+                    if maybe_make_parens_invisible_in_atom(
+                        child,
+                        parent=node,
+                        remove_brackets_around_comma=False,
+                    ):
+                        wrap_in_parentheses(node, child, visible=False)
+                else:
+                    wrap_in_parentheses(node, child, visible=False)
+                is_return_annotation = False
+
+        for child in node.children:
+            yield from self.visit(child)
+
+    def visit_match_case(self, node: Node) -> Iterator[Line]:
+        """Visit either a match or case statement."""
+        normalize_invisible_parens(
+            node, parens_after=set(), mode=self.mode, features=self.features
+        )
+
+        yield from self.line()
+        for child in node.children:
+            yield from self.visit(child)
+
     def visit_suite(self, node: Node) -> Iterator[Line]:
         """Visit a suite."""
-        if self.mode.is_pyi and is_stub_suite(node):
+        if (
+            self.mode.is_pyi or Preview.dummy_implementations in self.mode
+        ) and is_stub_suite(node):
             yield from self.visit(node.children[2])
         else:
             yield from self.visit_default(node)
 
     def visit_simple_stmt(self, node: Node) -> Iterator[Line]:
         """Visit a statement without nested statements."""
-        if first_child_is_arith(node):
-            wrap_in_parentheses(node, node.children[0], visible=False)
+        prev_type: Optional[int] = None
+        for child in node.children:
+            if (prev_type is None or prev_type == token.SEMI) and is_arith_like(child):
+                wrap_in_parentheses(node, child, visible=False)
+            prev_type = child.type
+
         is_suite_like = node.parent and node.parent.type in STATEMENT
         if is_suite_like:
-            if self.mode.is_pyi and is_stub_body(node):
+            if (
+                self.mode.is_pyi or Preview.dummy_implementations in self.mode
+            ) and is_stub_body(node):
                 yield from self.visit_default(node)
             else:
                 yield from self.line(+1)
@@ -163,7 +310,7 @@ class LineGenerator(Visitor[Line]):
 
         else:
             if (
-                not self.mode.is_pyi
+                not (self.mode.is_pyi or Preview.dummy_implementations in self.mode)
                 or not node.parent
                 or not is_stub_suite(node.parent)
             ):
@@ -178,12 +325,17 @@ class LineGenerator(Visitor[Line]):
         for child in children:
             yield from self.visit(child)
 
-            if child.type == token.ASYNC:
+            if child.type == token.ASYNC or child.type == STANDALONE_COMMENT:
+                # STANDALONE_COMMENT happens when `# fmt: skip` is applied on the async
+                # line.
                 break
 
         internal_stmt = next(children)
-        for child in internal_stmt.children:
-            yield from self.visit(child)
+        if Preview.improved_async_statements_handling in self.mode:
+            yield from self.visit(internal_stmt)
+        else:
+            for child in internal_stmt.children:
+                yield from self.visit(child)
 
     def visit_decorators(self, node: Node) -> Iterator[Line]:
         """Visit decorators."""
@@ -191,6 +343,30 @@ class LineGenerator(Visitor[Line]):
             yield from self.line()
             yield from self.visit(child)
 
+    def visit_power(self, node: Node) -> Iterator[Line]:
+        for idx, leaf in enumerate(node.children[:-1]):
+            next_leaf = node.children[idx + 1]
+
+            if not isinstance(leaf, Leaf):
+                continue
+
+            value = leaf.value.lower()
+            if (
+                leaf.type == token.NUMBER
+                and next_leaf.type == syms.trailer
+                # Ensure that we are in an attribute trailer
+                and next_leaf.children[0].type == token.DOT
+                # It shouldn't wrap hexadecimal, binary and octal literals
+                and not value.startswith(("0x", "0b", "0o"))
+                # It shouldn't wrap complex literals
+                and "j" not in value
+            ):
+                wrap_in_parentheses(node, leaf)
+
+        remove_await_parens(node)
+
+        yield from self.visit_default(node)
+
     def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]:
         """Remove a semicolon and put the other statement on a separate line."""
         yield from self.line()
@@ -222,11 +398,42 @@ class LineGenerator(Visitor[Line]):
             node.insert_child(index, Node(syms.atom, [lpar, operand, rpar]))
         yield from self.visit_default(node)
 
+    def visit_tname(self, node: Node) -> Iterator[Line]:
+        """
+        Add potential parentheses around types in function parameter lists to be made
+        into real parentheses in case the type hint is too long to fit on a line
+        Examples:
+        def foo(a: int, b: float = 7): ...
+
+        ->
+
+        def foo(a: (int), b: (float) = 7): ...
+        """
+        if Preview.parenthesize_long_type_hints in self.mode:
+            assert len(node.children) == 3
+            if maybe_make_parens_invisible_in_atom(node.children[2], parent=node):
+                wrap_in_parentheses(node, node.children[2], visible=False)
+
+        yield from self.visit_default(node)
+
     def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
+        if Preview.hex_codes_in_unicode_sequences in self.mode:
+            normalize_unicode_escape_sequences(leaf)
+
         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.
-            docstring = normalize_string_prefix(leaf.value, self.remove_u_prefix)
+            if self.mode.string_normalization:
+                docstring = normalize_string_prefix(leaf.value)
+                # visit_default() does handle string normalization for us, but
+                # since this method acts differently depending on quote style (ex.
+                # see padding logic below), there's a possibility for unstable
+                # formatting as visit_default() is called *after*. To avoid a
+                # situation where this function formats a docstring differently on
+                # the second pass, normalize it early.
+                docstring = normalize_string_quotes(docstring)
+            else:
+                docstring = leaf.value
             prefix = get_string_prefix(docstring)
             docstring = docstring[len(prefix) :]  # Remove the prefix
             quote_char = docstring[0]
@@ -238,13 +445,14 @@ class LineGenerator(Visitor[Line]):
             quote_len = 1 if docstring[1] != quote_char else 3
             docstring = docstring[quote_len:-quote_len]
             docstring_started_empty = not docstring
+            indent = " " * 4 * self.current_line.depth
 
             if is_multiline_string(leaf):
-                indent = " " * 4 * self.current_line.depth
                 docstring = fix_docstring(docstring, indent)
             else:
                 docstring = docstring.strip()
 
+            has_trailing_backslash = False
             if docstring:
                 # Add some padding if the docstring starts / ends with a quote mark.
                 if docstring[0] == quote_char:
@@ -257,12 +465,37 @@ class LineGenerator(Visitor[Line]):
                         # Odd number of tailing backslashes, add some padding to
                         # avoid escaping the closing string quote.
                         docstring += " "
+                        has_trailing_backslash = True
             elif not docstring_started_empty:
                 docstring = " "
 
             # We could enforce triple quotes at this point.
             quote = quote_char * quote_len
-            leaf.value = prefix + quote + docstring + quote
+
+            # It's invalid to put closing single-character quotes on a new line.
+            if self.mode and quote_len == 3:
+                # We need to find the length of the last line of the docstring
+                # to find if we can add the closing quotes to the line without
+                # exceeding the maximum line length.
+                # If docstring is one line, we don't put the closing quotes on a
+                # separate line because it looks ugly (#3320).
+                lines = docstring.splitlines()
+                last_line_length = len(lines[-1]) if docstring else 0
+
+                # If adding closing quotes would cause the last line to exceed
+                # the maximum line length then put a line break before the
+                # closing quotes
+                if (
+                    len(lines) > 1
+                    and last_line_length + quote_len > self.mode.line_length
+                    and len(indent) + quote_len <= self.mode.line_length
+                    and not has_trailing_backslash
+                ):
+                    leaf.value = prefix + quote + docstring + "\n" + indent + quote
+                else:
+                    leaf.value = prefix + quote + docstring + quote
+            else:
+                leaf.value = prefix + quote + docstring + quote
 
         yield from self.visit_default(leaf)
 
@@ -281,17 +514,38 @@ class LineGenerator(Visitor[Line]):
         self.visit_try_stmt = partial(
             v, keywords={"try", "except", "else", "finally"}, parens=Ø
         )
-        self.visit_except_clause = partial(v, keywords={"except"}, parens=Ø)
-        self.visit_with_stmt = partial(v, keywords={"with"}, parens=Ø)
-        self.visit_funcdef = partial(v, keywords={"def"}, parens=Ø)
+        self.visit_except_clause = partial(v, keywords={"except"}, parens={"except"})
+        self.visit_with_stmt = partial(v, keywords={"with"}, parens={"with"})
         self.visit_classdef = partial(v, keywords={"class"}, parens=Ø)
-        self.visit_expr_stmt = partial(v, keywords=Ø, parens=ASSIGNMENTS)
+
+        # When this is moved out of preview, add ":" directly to ASSIGNMENTS in nodes.py
+        if Preview.parenthesize_long_type_hints in self.mode:
+            assignments = ASSIGNMENTS | {":"}
+        else:
+            assignments = ASSIGNMENTS
+        self.visit_expr_stmt = partial(v, keywords=Ø, parens=assignments)
+
         self.visit_return_stmt = partial(v, keywords={"return"}, parens={"return"})
         self.visit_import_from = partial(v, keywords=Ø, parens={"import"})
         self.visit_del_stmt = partial(v, keywords=Ø, parens={"del"})
         self.visit_async_funcdef = self.visit_async_stmt
         self.visit_decorated = self.visit_decorators
 
+        # PEP 634
+        self.visit_match_stmt = self.visit_match_case
+        self.visit_case_block = self.visit_match_case
+
+
+def _hugging_power_ops_line_to_string(
+    line: Line,
+    features: Collection[Feature],
+    mode: Mode,
+) -> Optional[str]:
+    try:
+        return line_to_string(next(hug_power_op(line, features, mode)))
+    except CannotTransform:
+        return None
+
 
 def transform_line(
     line: Line, mode: Mode, features: Collection[Feature] = ()
@@ -308,6 +562,14 @@ def transform_line(
 
     line_str = line_to_string(line)
 
+    # We need the line string when power operators are hugging to determine if we should
+    # split the line. Default to line_str, if no power operator are present on the line.
+    line_str_hugging_power_ops = (
+        (_hugging_power_ops_line_to_string(line, features, mode) or line_str)
+        if Preview.fix_power_op_line_length in mode
+        else line_str
+    )
+
     ll = mode.line_length
     sn = mode.string_normalization
     string_merge = StringMerger(ll, sn)
@@ -321,21 +583,24 @@ def transform_line(
         and not line.should_split_rhs
         and not line.magic_trailing_comma
         and (
-            is_line_short_enough(line, line_length=mode.line_length, line_str=line_str)
+            is_line_short_enough(line, mode=mode, line_str=line_str_hugging_power_ops)
             or line.contains_unsplittable_type_ignore()
         )
         and not (line.inside_brackets and line.contains_standalone_comments())
+        and not line.contains_implicit_multiline_string_with_comments()
     ):
         # Only apply basic string preprocessing, since lines shouldn't be split here.
-        if mode.experimental_string_processing:
+        if Preview.string_processing in mode:
             transformers = [string_merge, string_paren_strip]
         else:
             transformers = []
-    elif line.is_def:
+    elif line.is_def and not should_split_funcdef_with_rhs(line, mode):
         transformers = [left_hand_split]
     else:
 
-        def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]:
+        def _rhs(
+            self: object, line: Line, features: Collection[Feature], mode: Mode
+        ) -> Iterator[Line]:
             """Wraps calls to `right_hand_split`.
 
             The calls increasingly `omit` right-hand trailers (bracket pairs with
@@ -343,14 +608,12 @@ def transform_line(
             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)
-                )
+                lines = list(right_hand_split(line, mode, 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):
+                if is_line_short_enough(lines[0], mode=mode):
                     yield from lines
                     return
 
@@ -358,11 +621,15 @@ def transform_line(
             # This mostly happens to multiline strings that are by definition
             # 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
-            )
+            yield from right_hand_split(line, mode, features=features)
 
-        if mode.experimental_string_processing:
+        # HACK: nested functions (like _rhs) compiled by mypyc don't retain their
+        # __name__ attribute which is needed in `run_transformer` further down.
+        # Unfortunately a nested class breaks mypyc too. So a class must be created
+        # via type ... https://github.com/mypyc/mypyc/issues/884
+        rhs = type("rhs", (), {"__call__": _rhs})()
+
+        if Preview.string_processing in mode:
             if line.inside_brackets:
                 transformers = [
                     string_merge,
@@ -386,6 +653,9 @@ def transform_line(
                 transformers = [delimiter_split, standalone_comment_split, rhs]
             else:
                 transformers = [rhs]
+    # It's always safe to attempt hugging of power operations and pretty much every line
+    # could match.
+    transformers.append(hug_power_op)
 
     for transform in transformers:
         # We are accumulating lines in `result` because we might want to abort
@@ -403,7 +673,49 @@ def transform_line(
         yield line
 
 
-def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator[Line]:
+def should_split_funcdef_with_rhs(line: Line, mode: Mode) -> bool:
+    """If a funcdef has a magic trailing comma in the return type, then we should first
+    split the line with rhs to respect the comma.
+    """
+    if Preview.respect_magic_trailing_comma_in_return_type not in mode:
+        return False
+
+    return_type_leaves: List[Leaf] = []
+    in_return_type = False
+
+    for leaf in line.leaves:
+        if leaf.type == token.COLON:
+            in_return_type = False
+        if in_return_type:
+            return_type_leaves.append(leaf)
+        if leaf.type == token.RARROW:
+            in_return_type = True
+
+    # using `bracket_split_build_line` will mess with whitespace, so we duplicate a
+    # couple lines from it.
+    result = Line(mode=line.mode, depth=line.depth)
+    leaves_to_track = get_leaves_inside_matching_brackets(return_type_leaves)
+    for leaf in return_type_leaves:
+        result.append(
+            leaf,
+            preformatted=True,
+            track_bracket=id(leaf) in leaves_to_track,
+        )
+
+    # we could also return true if the line is too long, and the return type is longer
+    # than the param list. Or if `should_split_rhs` returns True.
+    return result.magic_trailing_comma is not None
+
+
+class _BracketSplitComponent(Enum):
+    head = auto()
+    body = auto()
+    tail = auto()
+
+
+def left_hand_split(
+    line: Line, _features: Collection[Feature], mode: Mode
+) -> Iterator[Line]:
     """Split line into many lines, starting with the first matching bracket pair.
 
     Note: this usually looks weird, only use this for function definitions.
@@ -420,7 +732,10 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator
             current_leaves is body_leaves
             and leaf.type in CLOSING_BRACKETS
             and leaf.opening_bracket is matching_bracket
+            and isinstance(matching_bracket, Leaf)
         ):
+            ensure_visible(leaf)
+            ensure_visible(matching_bracket)
             current_leaves = tail_leaves if body_leaves else head_leaves
         current_leaves.append(leaf)
         if current_leaves is head_leaves:
@@ -430,9 +745,15 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator
     if not matching_bracket:
         raise CannotSplit("No brackets found")
 
-    head = bracket_split_build_line(head_leaves, line, matching_bracket)
-    body = bracket_split_build_line(body_leaves, line, matching_bracket, is_body=True)
-    tail = bracket_split_build_line(tail_leaves, line, matching_bracket)
+    head = bracket_split_build_line(
+        head_leaves, line, matching_bracket, component=_BracketSplitComponent.head
+    )
+    body = bracket_split_build_line(
+        body_leaves, line, matching_bracket, component=_BracketSplitComponent.body
+    )
+    tail = bracket_split_build_line(
+        tail_leaves, line, matching_bracket, component=_BracketSplitComponent.tail
+    )
     bracket_split_succeeded_or_raise(head, body, tail)
     for result in (head, body, tail):
         if result:
@@ -441,7 +762,7 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator
 
 def right_hand_split(
     line: Line,
-    line_length: int,
+    mode: Mode,
     features: Collection[Feature] = (),
     omit: Collection[LeafID] = (),
 ) -> Iterator[Line]:
@@ -453,6 +774,22 @@ def right_hand_split(
 
     Note: running this function modifies `bracket_depth` on the leaves of `line`.
     """
+    rhs_result = _first_right_hand_split(line, omit=omit)
+    yield from _maybe_split_omitting_optional_parens(
+        rhs_result, line, mode, features=features, omit=omit
+    )
+
+
+def _first_right_hand_split(
+    line: Line,
+    omit: Collection[LeafID] = (),
+) -> RHSResult:
+    """Split the line into head, body, tail starting with the last bracket pair.
+
+    Note: this function should not have side effects. It's relied upon by
+    _maybe_split_omitting_optional_parens to get an opinion whether to prefer
+    splitting on the right side of an assignment statement.
+    """
     tail_leaves: List[Leaf] = []
     body_leaves: List[Leaf] = []
     head_leaves: List[Leaf] = []
@@ -478,41 +815,82 @@ def right_hand_split(
     tail_leaves.reverse()
     body_leaves.reverse()
     head_leaves.reverse()
-    head = bracket_split_build_line(head_leaves, line, opening_bracket)
-    body = bracket_split_build_line(body_leaves, line, opening_bracket, is_body=True)
-    tail = bracket_split_build_line(tail_leaves, line, opening_bracket)
+    head = bracket_split_build_line(
+        head_leaves, line, opening_bracket, component=_BracketSplitComponent.head
+    )
+    body = bracket_split_build_line(
+        body_leaves, line, opening_bracket, component=_BracketSplitComponent.body
+    )
+    tail = bracket_split_build_line(
+        tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail
+    )
     bracket_split_succeeded_or_raise(head, body, tail)
+    return RHSResult(head, body, tail, opening_bracket, closing_bracket)
+
+
+def _maybe_split_omitting_optional_parens(
+    rhs: RHSResult,
+    line: Line,
+    mode: Mode,
+    features: Collection[Feature] = (),
+    omit: Collection[LeafID] = (),
+) -> Iterator[Line]:
     if (
         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
+        and rhs.opening_bracket.type == token.LPAR
+        and not rhs.opening_bracket.value
         # the closing bracket is an optional paren
-        and closing_bracket.type == token.RPAR
-        and not closing_bracket.value
+        and rhs.closing_bracket.type == token.RPAR
+        and not rhs.closing_bracket.value
         # it's not an import (optional parens are the only thing we can split on
         # in this case; attempting a split without them is a waste of time)
         and not line.is_import
         # there are no standalone comments in the body
-        and not body.contains_standalone_comments(0)
+        and not rhs.body.contains_standalone_comments(0)
         # and we can actually remove the parens
-        and can_omit_invisible_parens(body, line_length, omit_on_explode=omit)
+        and can_omit_invisible_parens(rhs, mode.line_length)
     ):
-        omit = {id(closing_bracket), *omit}
+        omit = {id(rhs.closing_bracket), *omit}
         try:
-            yield from right_hand_split(line, line_length, features=features, omit=omit)
-            return
+            # The RHSResult Omitting Optional Parens.
+            rhs_oop = _first_right_hand_split(line, omit=omit)
+            if not (
+                Preview.prefer_splitting_right_hand_side_of_assignments in line.mode
+                # the split is right after `=`
+                and len(rhs.head.leaves) >= 2
+                and rhs.head.leaves[-2].type == token.EQUAL
+                # the left side of assignment contains brackets
+                and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1])
+                # the left side of assignment is short enough (the -1 is for the ending
+                # optional paren)
+                and is_line_short_enough(
+                    rhs.head, mode=replace(mode, line_length=mode.line_length - 1)
+                )
+                # the left side of assignment won't explode further because of magic
+                # trailing comma
+                and rhs.head.magic_trailing_comma is None
+                # the split by omitting optional parens isn't preferred by some other
+                # reason
+                and not _prefer_split_rhs_oop(rhs_oop, mode)
+            ):
+                yield from _maybe_split_omitting_optional_parens(
+                    rhs_oop, line, mode, features=features, omit=omit
+                )
+                return
 
         except CannotSplit as e:
             if not (
-                can_be_split(body)
-                or is_line_short_enough(body, line_length=line_length)
+                can_be_split(rhs.body) or is_line_short_enough(rhs.body, mode=mode)
             ):
                 raise CannotSplit(
                     "Splitting failed, body is still too long and can't be split."
                 ) from e
 
-            elif head.contains_multiline_strings() or tail.contains_multiline_strings():
+            elif (
+                rhs.head.contains_multiline_strings()
+                or rhs.tail.contains_multiline_strings()
+            ):
                 raise CannotSplit(
                     "The current optional pair of parentheses is bound to fail to"
                     " satisfy the splitting algorithm because the head or the tail"
@@ -520,13 +898,42 @@ def right_hand_split(
                     " line."
                 ) from e
 
-    ensure_visible(opening_bracket)
-    ensure_visible(closing_bracket)
-    for result in (head, body, tail):
+    ensure_visible(rhs.opening_bracket)
+    ensure_visible(rhs.closing_bracket)
+    for result in (rhs.head, rhs.body, rhs.tail):
         if result:
             yield result
 
 
+def _prefer_split_rhs_oop(rhs_oop: RHSResult, mode: Mode) -> bool:
+    """
+    Returns whether we should prefer the result from a split omitting optional parens.
+    """
+    has_closing_bracket_after_assign = False
+    for leaf in reversed(rhs_oop.head.leaves):
+        if leaf.type == token.EQUAL:
+            break
+        if leaf.type in CLOSING_BRACKETS:
+            has_closing_bracket_after_assign = True
+            break
+    return (
+        # contains matching brackets after the `=` (done by checking there is a
+        # closing bracket)
+        has_closing_bracket_after_assign
+        or (
+            # the split is actually from inside the optional parens (done by checking
+            # the first line still contains the `=`)
+            any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves)
+            # the first line is short enough
+            and is_line_short_enough(rhs_oop.head, mode=mode)
+        )
+        # contains unsplittable type ignore
+        or rhs_oop.head.contains_unsplittable_type_ignore()
+        or rhs_oop.body.contains_unsplittable_type_ignore()
+        or rhs_oop.tail.contains_unsplittable_type_ignore()
+    )
+
+
 def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None:
     """Raise :exc:`CannotSplit` if the last left- or right-hand split failed.
 
@@ -554,15 +961,23 @@ def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None
 
 
 def bracket_split_build_line(
-    leaves: List[Leaf], original: Line, opening_bracket: Leaf, *, is_body: bool = False
+    leaves: List[Leaf],
+    original: Line,
+    opening_bracket: Leaf,
+    *,
+    component: _BracketSplitComponent,
 ) -> Line:
     """Return a new line with given `leaves` and respective comments from `original`.
 
-    If `is_body` is True, the result line is one-indented inside brackets and as such
-    has its first leaf's prefix normalized and a trailing comma added when expected.
+    If it's the head component, brackets will be tracked so trailing commas are
+    respected.
+
+    If it's the body component, the result line is one-indented inside brackets and as
+    such has its first leaf's prefix normalized and a trailing comma added when
+    expected.
     """
     result = Line(mode=original.mode, depth=original.depth)
-    if is_body:
+    if component is _BracketSplitComponent.body:
         result.inside_brackets = True
         result.depth += 1
         if leaves:
@@ -588,6 +1003,13 @@ def bracket_split_build_line(
                     )
                     if isinstance(node, Node) and isinstance(node.prev_sibling, Leaf)
                 )
+                # Except the false negatives above for PEP 604 unions where we
+                # can't add the comma.
+                and not (
+                    leaves[0].parent
+                    and leaves[0].parent.next_sibling
+                    and leaves[0].parent.next_sibling.type == token.VBAR
+                )
             )
 
             if original.is_import or no_commas:
@@ -600,12 +1022,21 @@ def bracket_split_build_line(
                         leaves.insert(i + 1, new_comma)
                     break
 
+    leaves_to_track: Set[LeafID] = set()
+    if component is _BracketSplitComponent.head:
+        leaves_to_track = get_leaves_inside_matching_brackets(leaves)
     # Populate the line
     for leaf in leaves:
-        result.append(leaf, preformatted=True)
+        result.append(
+            leaf,
+            preformatted=True,
+            track_bracket=id(leaf) in leaves_to_track,
+        )
         for comment_after in original.comments_after(leaf):
             result.append(comment_after, preformatted=True)
-    if is_body and should_split_line(result, opening_bracket):
+    if component is _BracketSplitComponent.body and should_split_line(
+        result, opening_bracket
+    ):
         result.should_split_rhs = True
     return result
 
@@ -617,16 +1048,39 @@ def dont_increase_indentation(split_func: Transformer) -> Transformer:
     """
 
     @wraps(split_func)
-    def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
-        for line in split_func(line, features):
-            normalize_prefix(line.leaves[0], inside_brackets=True)
-            yield line
+    def split_wrapper(
+        line: Line, features: Collection[Feature], mode: Mode
+    ) -> Iterator[Line]:
+        for split_line in split_func(line, features, mode):
+            normalize_prefix(split_line.leaves[0], inside_brackets=True)
+            yield split_line
 
     return split_wrapper
 
 
+def _get_last_non_comment_leaf(line: Line) -> Optional[int]:
+    for leaf_idx in range(len(line.leaves) - 1, 0, -1):
+        if line.leaves[leaf_idx].type != STANDALONE_COMMENT:
+            return leaf_idx
+    return None
+
+
+def _safe_add_trailing_comma(safe: bool, delimiter_priority: int, line: Line) -> Line:
+    if (
+        safe
+        and delimiter_priority == COMMA_PRIORITY
+        and line.leaves[-1].type != token.COMMA
+        and line.leaves[-1].type != STANDALONE_COMMENT
+    ):
+        new_comma = Leaf(token.COMMA, ",")
+        line.append(new_comma)
+    return line
+
+
 @dont_increase_indentation
-def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
+def delimiter_split(
+    line: Line, features: Collection[Feature], mode: Mode
+) -> Iterator[Line]:
     """Split according to delimiters of the highest priority.
 
     If the appropriate Features are given, the split will add trailing commas
@@ -666,7 +1120,8 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[
             )
             current_line.append(leaf)
 
-    for leaf in line.leaves:
+    last_non_comment_leaf = _get_last_non_comment_leaf(line)
+    for leaf_idx, leaf in enumerate(line.leaves):
         yield from append_to_line(leaf)
 
         for comment_after in line.comments_after(leaf):
@@ -683,6 +1138,15 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[
                     trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features
                 )
 
+        if (
+            Preview.add_trailing_comma_consistently in mode
+            and last_leaf.type == STANDALONE_COMMENT
+            and leaf_idx == last_non_comment_leaf
+        ):
+            current_line = _safe_add_trailing_comma(
+                trailing_comma_safe, delimiter_priority, current_line
+            )
+
         leaf_priority = bt.delimiters.get(id(leaf))
         if leaf_priority == delimiter_priority:
             yield current_line
@@ -691,20 +1155,15 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[
                 mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
             )
     if current_line:
-        if (
-            trailing_comma_safe
-            and delimiter_priority == COMMA_PRIORITY
-            and current_line.leaves[-1].type != token.COMMA
-            and current_line.leaves[-1].type != STANDALONE_COMMENT
-        ):
-            new_comma = Leaf(token.COMMA, ",")
-            current_line.append(new_comma)
+        current_line = _safe_add_trailing_comma(
+            trailing_comma_safe, delimiter_priority, current_line
+        )
         yield current_line
 
 
 @dont_increase_indentation
 def standalone_comment_split(
-    line: Line, features: Collection[Feature] = ()
+    line: Line, features: Collection[Feature], mode: Mode
 ) -> Iterator[Line]:
     """Split standalone comments from the rest of the line."""
     if not line.contains_standalone_comments(0):
@@ -755,7 +1214,9 @@ def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
     leaf.prefix = ""
 
 
-def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
+def normalize_invisible_parens(
+    node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature]
+) -> None:
     """Make existing optional parentheses invisible or create new ones.
 
     `parens_after` is a set of string leaf values immediately after which parens
@@ -768,12 +1229,21 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
         if pc.value in FMT_OFF:
             # This `node` has a prefix with `# fmt: off`, don't mess with parens.
             return
+
+    # The multiple context managers grammar has a different pattern, thus this is
+    # separate from the for-loop below. This possibly wraps them in invisible parens,
+    # and later will be removed in remove_with_parens when needed.
+    if node.type == syms.with_stmt:
+        _maybe_wrap_cms_in_parens(node, mode, features)
+
     check_lpar = False
     for index, child in enumerate(list(node.children)):
         # Fixes a bug where invisible parens are not properly stripped from
         # assignment statements that contain type annotations.
         if isinstance(child, Node) and child.type == syms.annassign:
-            normalize_invisible_parens(child, parens_after=parens_after)
+            normalize_invisible_parens(
+                child, parens_after=parens_after, mode=mode, features=features
+            )
 
         # Add parentheses around long tuple unpacking in assignments.
         if (
@@ -784,45 +1254,212 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
             check_lpar = True
 
         if check_lpar:
-            if child.type == syms.atom:
-                if maybe_make_parens_invisible_in_atom(child, parent=node):
+            if (
+                child.type == syms.atom
+                and node.type == syms.for_stmt
+                and isinstance(child.prev_sibling, Leaf)
+                and child.prev_sibling.type == token.NAME
+                and child.prev_sibling.value == "for"
+            ):
+                if maybe_make_parens_invisible_in_atom(
+                    child,
+                    parent=node,
+                    remove_brackets_around_comma=True,
+                ):
+                    wrap_in_parentheses(node, child, visible=False)
+            elif isinstance(child, Node) and node.type == syms.with_stmt:
+                remove_with_parens(child, node)
+            elif child.type == syms.atom:
+                if maybe_make_parens_invisible_in_atom(
+                    child,
+                    parent=node,
+                ):
                     wrap_in_parentheses(node, child, visible=False)
             elif is_one_tuple(child):
                 wrap_in_parentheses(node, child, visible=True)
             elif node.type == syms.import_from:
-                # "import from" nodes store parentheses directly as part of
-                # the statement
-                if child.type == token.LPAR:
-                    # make parentheses invisible
-                    child.value = ""  # type: ignore
-                    node.children[-1].value = ""  # type: ignore
-                elif child.type != token.STAR:
-                    # insert invisible parentheses
-                    node.insert_child(index, Leaf(token.LPAR, ""))
-                    node.append_child(Leaf(token.RPAR, ""))
+                _normalize_import_from(node, child, index)
                 break
+            elif (
+                index == 1
+                and child.type == token.STAR
+                and node.type == syms.except_clause
+            ):
+                # In except* (PEP 654), the star is actually part of
+                # of the keyword. So we need to skip the insertion of
+                # invisible parentheses to work more precisely.
+                continue
 
             elif not (isinstance(child, Leaf) and is_multiline_string(child)):
                 wrap_in_parentheses(node, child, visible=False)
 
-        check_lpar = isinstance(child, Leaf) and child.value in parens_after
+        comma_check = child.type == token.COMMA
+
+        check_lpar = isinstance(child, Leaf) and (
+            child.value in parens_after or comma_check
+        )
+
+
+def _normalize_import_from(parent: Node, child: LN, index: int) -> None:
+    # "import from" nodes store parentheses directly as part of
+    # the statement
+    if is_lpar_token(child):
+        assert is_rpar_token(parent.children[-1])
+        # make parentheses invisible
+        child.value = ""
+        parent.children[-1].value = ""
+    elif child.type != token.STAR:
+        # insert invisible parentheses
+        parent.insert_child(index, Leaf(token.LPAR, ""))
+        parent.append_child(Leaf(token.RPAR, ""))
 
 
-def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool:
+def remove_await_parens(node: Node) -> None:
+    if node.children[0].type == token.AWAIT and len(node.children) > 1:
+        if (
+            node.children[1].type == syms.atom
+            and node.children[1].children[0].type == token.LPAR
+        ):
+            if maybe_make_parens_invisible_in_atom(
+                node.children[1],
+                parent=node,
+                remove_brackets_around_comma=True,
+            ):
+                wrap_in_parentheses(node, node.children[1], visible=False)
+
+            # Since await is an expression we shouldn't remove
+            # brackets in cases where this would change
+            # the AST due to operator precedence.
+            # Therefore we only aim to remove brackets around
+            # power nodes that aren't also await expressions themselves.
+            # https://peps.python.org/pep-0492/#updated-operator-precedence-table
+            # N.B. We've still removed any redundant nested brackets though :)
+            opening_bracket = cast(Leaf, node.children[1].children[0])
+            closing_bracket = cast(Leaf, node.children[1].children[-1])
+            bracket_contents = node.children[1].children[1]
+            if isinstance(bracket_contents, Node):
+                if bracket_contents.type != syms.power:
+                    ensure_visible(opening_bracket)
+                    ensure_visible(closing_bracket)
+                elif (
+                    bracket_contents.type == syms.power
+                    and bracket_contents.children[0].type == token.AWAIT
+                ):
+                    ensure_visible(opening_bracket)
+                    ensure_visible(closing_bracket)
+                    # If we are in a nested await then recurse down.
+                    remove_await_parens(bracket_contents)
+
+
+def _maybe_wrap_cms_in_parens(
+    node: Node, mode: Mode, features: Collection[Feature]
+) -> None:
+    """When enabled and safe, wrap the multiple context managers in invisible parens.
+
+    It is only safe when `features` contain Feature.PARENTHESIZED_CONTEXT_MANAGERS.
+    """
+    if (
+        Feature.PARENTHESIZED_CONTEXT_MANAGERS not in features
+        or Preview.wrap_multiple_context_managers_in_parens not in mode
+        or len(node.children) <= 2
+        # If it's an atom, it's already wrapped in parens.
+        or node.children[1].type == syms.atom
+    ):
+        return
+    colon_index: Optional[int] = None
+    for i in range(2, len(node.children)):
+        if node.children[i].type == token.COLON:
+            colon_index = i
+            break
+    if colon_index is not None:
+        lpar = Leaf(token.LPAR, "")
+        rpar = Leaf(token.RPAR, "")
+        context_managers = node.children[1:colon_index]
+        for child in context_managers:
+            child.remove()
+        # After wrapping, the with_stmt will look like this:
+        #   with_stmt
+        #     NAME 'with'
+        #     atom
+        #       LPAR ''
+        #       testlist_gexp
+        #         ... <-- context_managers
+        #       /testlist_gexp
+        #       RPAR ''
+        #     /atom
+        #     COLON ':'
+        new_child = Node(
+            syms.atom, [lpar, Node(syms.testlist_gexp, context_managers), rpar]
+        )
+        node.insert_child(1, new_child)
+
+
+def remove_with_parens(node: Node, parent: Node) -> None:
+    """Recursively hide optional parens in `with` statements."""
+    # Removing all unnecessary parentheses in with statements in one pass is a tad
+    # complex as different variations of bracketed statements result in pretty
+    # different parse trees:
+    #
+    # with (open("file")) as f:                       # this is an asexpr_test
+    #     ...
+    #
+    # with (open("file") as f):                       # this is an atom containing an
+    #     ...                                         # asexpr_test
+    #
+    # with (open("file")) as f, (open("file")) as f:  # this is asexpr_test, COMMA,
+    #     ...                                         # asexpr_test
+    #
+    # with (open("file") as f, open("file") as f):    # an atom containing a
+    #     ...                                         # testlist_gexp which then
+    #                                                 # contains multiple asexpr_test(s)
+    if node.type == syms.atom:
+        if maybe_make_parens_invisible_in_atom(
+            node,
+            parent=parent,
+            remove_brackets_around_comma=True,
+        ):
+            wrap_in_parentheses(parent, node, visible=False)
+        if isinstance(node.children[1], Node):
+            remove_with_parens(node.children[1], node)
+    elif node.type == syms.testlist_gexp:
+        for child in node.children:
+            if isinstance(child, Node):
+                remove_with_parens(child, node)
+    elif node.type == syms.asexpr_test and not any(
+        leaf.type == token.COLONEQUAL for leaf in node.leaves()
+    ):
+        if maybe_make_parens_invisible_in_atom(
+            node.children[0],
+            parent=node,
+            remove_brackets_around_comma=True,
+        ):
+            wrap_in_parentheses(node, node.children[0], visible=False)
+
+
+def maybe_make_parens_invisible_in_atom(
+    node: LN,
+    parent: LN,
+    remove_brackets_around_comma: bool = False,
+) -> bool:
     """If it's safe, make the parens in the atom `node` invisible, recursively.
     Additionally, remove repeated, adjacent invisible parens from the atom `node`
     as they are redundant.
 
     Returns whether the node should itself be wrapped in invisible parentheses.
-
     """
-
     if (
-        node.type != syms.atom
+        node.type not in (syms.atom, syms.expr)
         or is_empty_tuple(node)
         or is_one_tuple(node)
         or (is_yield(node) and parent.type != syms.expr_stmt)
-        or max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY
+        or (
+            # This condition tries to prevent removing non-optional brackets
+            # around a tuple, however, can be a bit overzealous so we provide
+            # and option to skip this check for `for` and `with` statements.
+            not remove_brackets_around_comma
+            and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY
+        )
+        or is_tuple_containing_walrus(node)
     ):
         return False
 
@@ -832,20 +1469,34 @@ def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> bool:
             syms.expr_stmt,
             syms.assert_stmt,
             syms.return_stmt,
+            syms.except_clause,
+            syms.funcdef,
+            syms.with_stmt,
+            syms.tname,
             # these ones aren't useful to end users, but they do please fuzzers
             syms.for_stmt,
             syms.del_stmt,
+            syms.for_stmt,
         ]:
             return False
 
     first = node.children[0]
     last = node.children[-1]
-    if first.type == token.LPAR and last.type == token.RPAR:
+    if is_lpar_token(first) and is_rpar_token(last):
         middle = node.children[1]
         # make parentheses invisible
-        first.value = ""  # type: ignore
-        last.value = ""  # type: ignore
-        maybe_make_parens_invisible_in_atom(middle, parent=parent)
+        if (
+            # If the prefix of `middle` includes a type comment with
+            # ignore annotation, then we do not remove the parentheses
+            not is_type_ignore_comment_string(middle.prefix.strip())
+        ):
+            first.value = ""
+            last.value = ""
+        maybe_make_parens_invisible_in_atom(
+            middle,
+            parent=parent,
+            remove_brackets_around_comma=remove_brackets_around_comma,
+        )
 
         if is_atom_with_invisible_parens(middle):
             # Strip the invisible parens from `middle` by replacing
@@ -920,7 +1571,8 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf
                 if (
                     prev
                     and prev.type == token.COMMA
-                    and not is_one_tuple_between(
+                    and leaf.opening_bracket is not None
+                    and not is_one_sequence_between(
                         leaf.opening_bracket, leaf, line.leaves
                     )
                 ):
@@ -947,7 +1599,8 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf
             if (
                 prev
                 and prev.type == token.COMMA
-                and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves)
+                and leaf.opening_bracket is not None
+                and not is_one_sequence_between(leaf.opening_bracket, leaf, line.leaves)
             ):
                 # Never omit bracket pairs with trailing commas.
                 # We need to explode on those.
@@ -969,20 +1622,22 @@ def run_transformer(
     if not line_str:
         line_str = line_to_string(line)
     result: List[Line] = []
-    for transformed_line in transform(line, features):
+    for transformed_line in transform(line, features, mode):
         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))
 
+    features_set = set(features)
     if (
-        transform.__name__ != "rhs"
+        Feature.FORCE_OPTIONAL_PARENTHESES in features_set
+        or transform.__class__.__name__ != "rhs"
         or not line.bracket_tracker.invisible
         or any(bracket.value for bracket in line.bracket_tracker.invisible)
         or line.contains_multiline_strings()
         or result[0].contains_uncollapsable_type_comments()
         or result[0].contains_unsplittable_type_ignore()
-        or is_line_short_enough(result[0], line_length=mode.line_length)
+        or is_line_short_enough(result[0], mode=mode)
         # If any leaves have no parents (which _can_ occur since
         # `transform(line)` potentially destroys the line's underlying node
         # structure), then we can't proceed. Doing so would cause the below
@@ -993,12 +1648,10 @@ def run_transformer(
 
     line_copy = line.clone()
     append_leaves(line_copy, line, line.leaves)
-    features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES}
+    features_fop = features_set | {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
-    ):
+    if all(is_line_short_enough(ln, mode=mode) for ln in second_opinion):
         result = second_opinion
     return result
index 63225c0e6d31288bd7c68004e751eb05ca537382..6acc95e7a7e004dbf2e35a571bb08def6f1100fe 100644 (file)
@@ -1,9 +1,9 @@
-from dataclasses import dataclass, field
 import itertools
+import math
 import sys
+from dataclasses import dataclass, field
 from typing import (
     Callable,
-    Collection,
     Dict,
     Iterator,
     List,
@@ -11,31 +11,45 @@ from typing import (
     Sequence,
     Tuple,
     TypeVar,
+    Union,
     cast,
 )
 
-from blib2to3.pytree import Node, Leaf
+from black.brackets import COMMA_PRIORITY, DOT_PRIORITY, BracketTracker
+from black.mode import Mode, Preview
+from black.nodes import (
+    BRACKETS,
+    CLOSING_BRACKETS,
+    OPENING_BRACKETS,
+    STANDALONE_COMMENT,
+    TEST_DESCENDANTS,
+    child_towards,
+    is_import,
+    is_multiline_string,
+    is_one_sequence_between,
+    is_type_comment,
+    is_type_ignore_comment,
+    is_with_or_async_with_stmt,
+    replace_child,
+    syms,
+    whitespace,
+)
+from black.strings import str_width
 from blib2to3.pgen2 import token
-
-from black.brackets import BracketTracker, DOT_PRIORITY
-from black.mode import Mode
-from black.nodes import STANDALONE_COMMENT, TEST_DESCENDANTS
-from black.nodes import BRACKETS, OPENING_BRACKETS, CLOSING_BRACKETS
-from black.nodes import syms, whitespace, replace_child, child_towards
-from black.nodes import is_multiline_string, is_import, is_type_comment, last_two_except
-from black.nodes import is_one_tuple_between
+from blib2to3.pytree import Leaf, Node
 
 # types
 T = TypeVar("T")
 Index = int
 LeafID = int
+LN = Union[Leaf, Node]
 
 
 @dataclass
 class Line:
     """Holds leaves and comments. Can be printed with `str(line)`."""
 
-    mode: Mode
+    mode: Mode = field(repr=False)
     depth: int = 0
     leaves: List[Leaf] = field(default_factory=list)
     # keys ordered like `leaves`
@@ -45,7 +59,9 @@ class Line:
     should_split_rhs: bool = False
     magic_trailing_comma: Optional[Leaf] = None
 
-    def append(self, leaf: Leaf, preformatted: bool = False) -> None:
+    def append(
+        self, leaf: Leaf, preformatted: bool = False, track_bracket: bool = False
+    ) -> None:
         """Add a new `leaf` to the end of the line.
 
         Unless `preformatted` is True, the `leaf` will receive a new consistent
@@ -65,9 +81,11 @@ class Line:
             # Note: at this point leaf.prefix should be empty except for
             # imports, for which we only preserve newlines.
             leaf.prefix += whitespace(
-                leaf, complex_subscript=self.is_complex_subscript(leaf)
+                leaf,
+                complex_subscript=self.is_complex_subscript(leaf),
+                mode=self.mode,
             )
-        if self.inside_brackets or not preformatted:
+        if self.inside_brackets or not preformatted or track_bracket:
             self.bracket_tracker.mark(leaf)
             if self.mode.magic_trailing_comma:
                 if self.has_magic_trailing_comma(leaf):
@@ -109,6 +127,11 @@ class Line:
         """Is this an import line?"""
         return bool(self) and is_import(self.leaves[0])
 
+    @property
+    def is_with_or_async_with_stmt(self) -> bool:
+        """Is this a with_stmt line?"""
+        return bool(self) and is_with_or_async_with_stmt(self.leaves[0])
+
     @property
     def is_class(self) -> bool:
         """Is this line a class definition?"""
@@ -144,6 +167,13 @@ class Line:
             and second_leaf.value == "def"
         )
 
+    @property
+    def is_stub_def(self) -> bool:
+        """Is this line a function definition with a body consisting only of "..."?"""
+        return self.is_def and self.leaves[-4:] == [Leaf(token.COLON, ":")] + [
+            Leaf(token.DOT, ".") for _ in range(3)
+        ]
+
     @property
     def is_class_paren_empty(self) -> bool:
         """Is this a class with no base classes but using parentheses?
@@ -163,10 +193,42 @@ class Line:
     @property
     def is_triple_quoted_string(self) -> bool:
         """Is the line a triple quoted string?"""
-        return (
-            bool(self)
-            and self.leaves[0].type == token.STRING
-            and self.leaves[0].value.startswith(('"""', "'''"))
+        if not self or self.leaves[0].type != token.STRING:
+            return False
+        value = self.leaves[0].value
+        if value.startswith(('"""', "'''")):
+            return True
+        if Preview.accept_raw_docstrings in self.mode and value.startswith(
+            ("r'''", 'r"""', "R'''", 'R"""')
+        ):
+            return True
+        return False
+
+    @property
+    def opens_block(self) -> bool:
+        """Does this line open a new level of indentation."""
+        if len(self.leaves) == 0:
+            return False
+        return self.leaves[-1].type == token.COLON
+
+    def is_fmt_pass_converted(
+        self, *, first_leaf_matches: Optional[Callable[[Leaf], bool]] = None
+    ) -> bool:
+        """Is this line converted from fmt off/skip code?
+
+        If first_leaf_matches is not None, it only returns True if the first
+        leaf of converted code matches.
+        """
+        if len(self.leaves) != 1:
+            return False
+        leaf = self.leaves[0]
+        if (
+            leaf.type != STANDALONE_COMMENT
+            or leaf.fmt_pass_converted_first_leaf is None
+        ):
+            return False
+        return first_leaf_matches is None or first_leaf_matches(
+            leaf.fmt_pass_converted_first_leaf
         )
 
     def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool:
@@ -177,6 +239,21 @@ class Line:
 
         return False
 
+    def contains_implicit_multiline_string_with_comments(self) -> bool:
+        """Chck if we have an implicit multiline string with comments on the line"""
+        for leaf_type, leaf_group_iterator in itertools.groupby(
+            self.leaves, lambda leaf: leaf.type
+        ):
+            if leaf_type != token.STRING:
+                continue
+            leaf_list = list(leaf_group_iterator)
+            if len(leaf_list) == 1:
+                continue
+            for leaf in leaf_list:
+                if self.comments_after(leaf):
+                    return True
+        return False
+
     def contains_uncollapsable_type_comments(self) -> bool:
         ignored_ids = set()
         try:
@@ -204,7 +281,7 @@ class Line:
             for comment in comments:
                 if is_type_comment(comment):
                     if comment_seen or (
-                        not is_type_comment(comment, " ignore")
+                        not is_type_ignore_comment(comment)
                         and leaf_id not in ignored_ids
                     ):
                         return True
@@ -241,7 +318,7 @@ class Line:
             # line.
             for node in self.leaves[-2:]:
                 for comment in self.comments.get(id(node), []):
-                    if is_type_comment(comment, " ignore"):
+                    if is_type_ignore_comment(comment):
                         return True
 
         return False
@@ -255,8 +332,10 @@ class Line:
         """Return True if we have a magic trailing comma, that is when:
         - there's a trailing comma here
         - it's not a one-tuple
+        - it's not a single-element subscript
         Additionally, if ensure_removable:
         - it's not from square bracket indexing
+        (specifically, single-element square bracket indexing)
         """
         if not (
             closing.type in CLOSING_BRACKETS
@@ -269,15 +348,42 @@ class Line:
             return True
 
         if closing.type == token.RSQB:
+            if (
+                closing.parent
+                and closing.parent.type == syms.trailer
+                and closing.opening_bracket
+                and is_one_sequence_between(
+                    closing.opening_bracket,
+                    closing,
+                    self.leaves,
+                    brackets=(token.LSQB, token.RSQB),
+                )
+            ):
+                return False
+
             if not ensure_removable:
                 return True
+
             comma = self.leaves[-1]
-            return bool(comma.parent and comma.parent.type == syms.listmaker)
+            if comma.parent is None:
+                return False
+            return (
+                comma.parent.type != syms.subscriptlist
+                or closing.opening_bracket is None
+                or not is_one_sequence_between(
+                    closing.opening_bracket,
+                    closing,
+                    self.leaves,
+                    brackets=(token.LSQB, token.RSQB),
+                )
+            )
 
         if self.is_import:
             return True
 
-        if not is_one_tuple_between(closing.opening_bracket, closing, self.leaves):
+        if closing.opening_bracket is not None and not is_one_sequence_between(
+            closing.opening_bracket, closing, self.leaves
+        ):
             return True
 
         return False
@@ -400,6 +506,39 @@ class Line:
         return bool(self.leaves or self.comments)
 
 
+@dataclass
+class RHSResult:
+    """Intermediate split result from a right hand split."""
+
+    head: Line
+    body: Line
+    tail: Line
+    opening_bracket: Leaf
+    closing_bracket: Leaf
+
+
+@dataclass
+class LinesBlock:
+    """Class that holds information about a block of formatted lines.
+
+    This is introduced so that the EmptyLineTracker can look behind the standalone
+    comments and adjust their empty lines for class or def lines.
+    """
+
+    mode: Mode
+    previous_block: Optional["LinesBlock"]
+    original_line: Line
+    before: int = 0
+    content_lines: List[str] = field(default_factory=list)
+    after: int = 0
+
+    def all_lines(self) -> List[str]:
+        empty_line = str(Line(mode=self.mode))
+        return (
+            [empty_line * self.before] + self.content_lines + [empty_line * self.after]
+        )
+
+
 @dataclass
 class EmptyLineTracker:
     """Provides a stateful method that returns the number of potential extra
@@ -410,33 +549,65 @@ class EmptyLineTracker:
     are consumed by `maybe_empty_lines()` and included in the computation.
     """
 
-    is_pyi: bool = False
+    mode: Mode
     previous_line: Optional[Line] = None
-    previous_after: int = 0
-    previous_defs: List[int] = field(default_factory=list)
+    previous_block: Optional[LinesBlock] = None
+    previous_defs: List[Line] = field(default_factory=list)
+    semantic_leading_comment: Optional[LinesBlock] = None
 
-    def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
+    def maybe_empty_lines(self, current_line: Line) -> LinesBlock:
         """Return the number of extra empty lines before and after the `current_line`.
 
         This is for separating `def`, `async def` and `class` with extra empty
         lines (two on module-level).
         """
         before, after = self._maybe_empty_lines(current_line)
+        previous_after = self.previous_block.after if self.previous_block else 0
         before = (
             # Black should not insert empty lines at the beginning
             # of the file
             0
             if self.previous_line is None
-            else before - self.previous_after
+            else before - previous_after
+        )
+        if (
+            Preview.module_docstring_newlines in current_line.mode
+            and self.previous_block
+            and self.previous_block.previous_block is None
+            and len(self.previous_block.original_line.leaves) == 1
+            and self.previous_block.original_line.is_triple_quoted_string
+        ):
+            before = 1
+
+        block = LinesBlock(
+            mode=self.mode,
+            previous_block=self.previous_block,
+            original_line=current_line,
+            before=before,
+            after=after,
         )
-        self.previous_after = after
+
+        # Maintain the semantic_leading_comment state.
+        if current_line.is_comment:
+            if self.previous_line is None or (
+                not self.previous_line.is_decorator
+                # `or before` means this comment already has an empty line before
+                and (not self.previous_line.is_comment or before)
+                and (self.semantic_leading_comment is None or before)
+            ):
+                self.semantic_leading_comment = block
+        # `or before` means this decorator already has an empty line before
+        elif not current_line.is_decorator or before:
+            self.semantic_leading_comment = None
+
         self.previous_line = current_line
-        return before, after
+        self.previous_block = block
+        return block
 
     def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
         max_allowed = 1
         if current_line.depth == 0:
-            max_allowed = 1 if self.is_pyi else 2
+            max_allowed = 1 if self.mode.is_pyi else 2
         if current_line.leaves:
             # Consume the first leaf's extra newlines.
             first_leaf = current_line.leaves[0]
@@ -445,20 +616,63 @@ class EmptyLineTracker:
             first_leaf.prefix = ""
         else:
             before = 0
+
+        user_had_newline = bool(before)
         depth = current_line.depth
-        while self.previous_defs and self.previous_defs[-1] >= depth:
-            self.previous_defs.pop()
-            if self.is_pyi:
-                before = 0 if depth else 1
+
+        previous_def = None
+        while self.previous_defs and self.previous_defs[-1].depth >= depth:
+            previous_def = self.previous_defs.pop()
+
+        if previous_def is not None:
+            assert self.previous_line is not None
+            if self.mode.is_pyi:
+                if depth and not current_line.is_def and self.previous_line.is_def:
+                    # Empty lines between attributes and methods should be preserved.
+                    before = 1 if user_had_newline else 0
+                elif (
+                    Preview.blank_line_after_nested_stub_class in self.mode
+                    and previous_def.is_class
+                    and not previous_def.is_stub_class
+                ):
+                    before = 1
+                elif depth:
+                    before = 0
+                else:
+                    before = 1
             else:
-                before = 1 if depth else 2
+                if depth:
+                    before = 1
+                elif (
+                    not depth
+                    and previous_def.depth
+                    and current_line.leaves[-1].type == token.COLON
+                    and (
+                        current_line.leaves[0].value
+                        not in ("with", "try", "for", "while", "if", "match")
+                    )
+                ):
+                    # We shouldn't add two newlines between an indented function and
+                    # a dependent non-indented clause. This is to avoid issues with
+                    # conditional function definitions that are technically top-level
+                    # and therefore get two trailing newlines, but look weird and
+                    # inconsistent when they're followed by elif, else, etc. This is
+                    # worse because these functions only get *one* preceding newline
+                    # already.
+                    before = 1
+                else:
+                    before = 2
+
         if current_line.is_decorator or current_line.is_def or current_line.is_class:
-            return self._maybe_empty_lines_for_class_or_def(current_line, before)
+            return self._maybe_empty_lines_for_class_or_def(
+                current_line, before, user_had_newline
+            )
 
         if (
             self.previous_line
             and self.previous_line.is_import
             and not current_line.is_import
+            and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import)
             and depth == self.previous_line.depth
         ):
             return (before or 1), 0
@@ -468,21 +682,25 @@ class EmptyLineTracker:
             and self.previous_line.is_class
             and current_line.is_triple_quoted_string
         ):
+            if Preview.no_blank_line_before_class_docstring in current_line.mode:
+                return 0, 1
             return before, 1
 
+        if self.previous_line and self.previous_line.opens_block:
+            return 0, 0
         return before, 0
 
-    def _maybe_empty_lines_for_class_or_def(
-        self, current_line: Line, before: int
+    def _maybe_empty_lines_for_class_or_def(  # noqa: C901
+        self, current_line: Line, before: int, user_had_newline: bool
     ) -> Tuple[int, int]:
         if not current_line.is_decorator:
-            self.previous_defs.append(current_line.depth)
+            self.previous_defs.append(current_line)
         if self.previous_line is None:
             # Don't insert empty lines before the first line in the file.
             return 0, 0
 
         if self.previous_line.is_decorator:
-            if self.is_pyi and current_line.is_stub_class:
+            if self.mode.is_pyi and current_line.is_stub_class:
                 # Insert an empty line after a decorated stub class
                 return 0, 1
 
@@ -493,34 +711,78 @@ class EmptyLineTracker:
         ):
             return 0, 0
 
+        comment_to_add_newlines: Optional[LinesBlock] = None
         if (
             self.previous_line.is_comment
             and self.previous_line.depth == current_line.depth
             and before == 0
         ):
-            return 0, 0
+            slc = self.semantic_leading_comment
+            if (
+                slc is not None
+                and slc.previous_block is not None
+                and not slc.previous_block.original_line.is_class
+                and not slc.previous_block.original_line.opens_block
+                and slc.before <= 1
+            ):
+                comment_to_add_newlines = slc
+            else:
+                return 0, 0
 
-        if self.is_pyi:
-            if self.previous_line.depth > current_line.depth:
-                newlines = 1
-            elif current_line.is_class or self.previous_line.is_class:
-                if current_line.is_stub_class and self.previous_line.is_stub_class:
+        if self.mode.is_pyi:
+            if current_line.is_class or self.previous_line.is_class:
+                if self.previous_line.depth < current_line.depth:
+                    newlines = 0
+                elif self.previous_line.depth > current_line.depth:
+                    newlines = 1
+                elif current_line.is_stub_class and self.previous_line.is_stub_class:
                     # No blank line between classes with an empty body
                     newlines = 0
                 else:
                     newlines = 1
+            # Remove case `self.previous_line.depth > current_line.depth` below when
+            # this becomes stable.
+            #
+            # Don't inspect the previous line if it's part of the body of the previous
+            # statement in the same level, we always want a blank line if there's
+            # something with a body preceding.
+            elif (
+                Preview.blank_line_between_nested_and_def_stub_file in current_line.mode
+                and self.previous_line.depth > current_line.depth
+            ):
+                newlines = 1
             elif (
                 current_line.is_def or current_line.is_decorator
             ) and not self.previous_line.is_def:
-                # Blank line between a block of functions (maybe with preceding
-                # decorators) and a block of non-functions
+                if current_line.depth:
+                    # In classes empty lines between attributes and methods should
+                    # be preserved.
+                    newlines = min(1, before)
+                else:
+                    # Blank line between a block of functions (maybe with preceding
+                    # decorators) and a block of non-functions
+                    newlines = 1
+            elif self.previous_line.depth > current_line.depth:
                 newlines = 1
             else:
                 newlines = 0
         else:
-            newlines = 2
-        if current_line.depth and newlines:
-            newlines -= 1
+            newlines = 1 if current_line.depth else 2
+            # If a user has left no space after a dummy implementation, don't insert
+            # new lines. This is useful for instance for @overload or Protocols.
+            if (
+                Preview.dummy_implementations in self.mode
+                and self.previous_line.is_stub_def
+                and not user_had_newline
+            ):
+                newlines = 0
+        if comment_to_add_newlines is not None:
+            previous_block = comment_to_add_newlines.previous_block
+            if previous_block is not None:
+                comment_to_add_newlines.before = (
+                    max(comment_to_add_newlines.before, newlines) - previous_block.after
+                )
+                newlines = 0
         return newlines, 0
 
 
@@ -556,18 +818,95 @@ def append_leaves(
             new_line.append(comment_leaf, preformatted=True)
 
 
-def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> bool:
-    """Return True if `line` is no longer than `line_length`.
-
+def is_line_short_enough(  # noqa: C901
+    line: Line, *, mode: Mode, line_str: str = ""
+) -> bool:
+    """For non-multiline strings, return True if `line` is no longer than `line_length`.
+    For multiline strings, looks at the context around `line` to determine
+    if it should be inlined or split up.
     Uses the provided `line_str` rendering, if any, otherwise computes a new one.
     """
     if not line_str:
         line_str = line_to_string(line)
-    return (
-        len(line_str) <= line_length
-        and "\n" not in line_str  # multiline strings
-        and not line.contains_standalone_comments()
-    )
+
+    width = str_width if mode.preview else len
+
+    if Preview.multiline_string_handling not in mode:
+        return (
+            width(line_str) <= mode.line_length
+            and "\n" not in line_str  # multiline strings
+            and not line.contains_standalone_comments()
+        )
+
+    if line.contains_standalone_comments():
+        return False
+    if "\n" not in line_str:
+        # No multiline strings (MLS) present
+        return width(line_str) <= mode.line_length
+
+    first, *_, last = line_str.split("\n")
+    if width(first) > mode.line_length or width(last) > mode.line_length:
+        return False
+
+    # Traverse the AST to examine the context of the multiline string (MLS),
+    # tracking aspects such as depth and comma existence,
+    # to determine whether to split the MLS or keep it together.
+    # Depth (which is based on the existing bracket_depth concept)
+    # is needed to determine nesting level of the MLS.
+    # Includes special case for trailing commas.
+    commas: List[int] = []  # tracks number of commas per depth level
+    multiline_string: Optional[Leaf] = None
+    # store the leaves that contain parts of the MLS
+    multiline_string_contexts: List[LN] = []
+
+    max_level_to_update: Union[int, float] = math.inf  # track the depth of the MLS
+    for i, leaf in enumerate(line.leaves):
+        if max_level_to_update == math.inf:
+            had_comma: Optional[int] = None
+            if leaf.bracket_depth + 1 > len(commas):
+                commas.append(0)
+            elif leaf.bracket_depth + 1 < len(commas):
+                had_comma = commas.pop()
+            if (
+                had_comma is not None
+                and multiline_string is not None
+                and multiline_string.bracket_depth == leaf.bracket_depth + 1
+            ):
+                # Have left the level with the MLS, stop tracking commas
+                max_level_to_update = leaf.bracket_depth
+                if had_comma > 0:
+                    # MLS was in parens with at least one comma - force split
+                    return False
+
+        if leaf.bracket_depth <= max_level_to_update and leaf.type == token.COMMA:
+            # Ignore non-nested trailing comma
+            # directly after MLS/MLS-containing expression
+            ignore_ctxs: List[Optional[LN]] = [None]
+            ignore_ctxs += multiline_string_contexts
+            if not (leaf.prev_sibling in ignore_ctxs and i == len(line.leaves) - 1):
+                commas[leaf.bracket_depth] += 1
+        if max_level_to_update != math.inf:
+            max_level_to_update = min(max_level_to_update, leaf.bracket_depth)
+
+        if is_multiline_string(leaf):
+            if len(multiline_string_contexts) > 0:
+                # >1 multiline string cannot fit on a single line - force split
+                return False
+            multiline_string = leaf
+            ctx: LN = leaf
+            # fetch the leaf components of the MLS in the AST
+            while str(ctx) in line_str:
+                multiline_string_contexts.append(ctx)
+                if ctx.parent is None:
+                    break
+                ctx = ctx.parent
+
+    # May not have a triple-quoted multiline string at all,
+    # in case of a regular string with embedded newlines and line continuations
+    if len(multiline_string_contexts) == 0:
+        return True
+
+    return all(val == 0 for val in commas)
 
 
 def can_be_split(line: Line) -> bool:
@@ -607,26 +946,42 @@ def can_be_split(line: Line) -> bool:
 
 
 def can_omit_invisible_parens(
-    line: Line,
+    rhs: RHSResult,
     line_length: int,
-    omit_on_explode: Collection[LeafID] = (),
 ) -> bool:
-    """Does `line` have a shape safe to reformat without optional parens around it?
+    """Does `rhs.body` have a shape safe to reformat without optional parens around it?
 
     Returns True for only a subset of potentially nice looking formattings but
     the point is to not return false positives that end up producing lines that
     are too long.
     """
+    line = rhs.body
     bt = line.bracket_tracker
     if not bt.delimiters:
         # Without delimiters the optional parentheses are useless.
         return True
 
     max_priority = bt.max_delimiter_priority()
-    if bt.delimiter_count_with_priority(max_priority) > 1:
+    delimiter_count = bt.delimiter_count_with_priority(max_priority)
+    if delimiter_count > 1:
         # With more than one delimiter of a kind the optional parentheses read better.
         return False
 
+    if delimiter_count == 1:
+        if (
+            Preview.wrap_multiple_context_managers_in_parens in line.mode
+            and max_priority == COMMA_PRIORITY
+            and rhs.head.is_with_or_async_with_stmt
+        ):
+            # For two context manager with statements, the optional parentheses read
+            # better. In this case, `rhs.body` is the context managers part of
+            # the with statement. `rhs.head` is the `with (` part on the previous
+            # line.
+            return False
+        # Otherwise it may also read better, but we don't do it today and requires
+        # careful considerations for all possible cases. See
+        # https://github.com/psf/black/issues/2156.
+
     if max_priority == DOT_PRIORITY:
         # A single stranded method call doesn't require optional parentheses.
         return True
@@ -647,12 +1002,6 @@ def can_omit_invisible_parens(
 
     penultimate = line.leaves[-2]
     last = line.leaves[-1]
-    if line.magic_trailing_comma:
-        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
@@ -674,10 +1023,6 @@ def can_omit_invisible_parens(
             # unnecessary.
             return True
 
-        if line.magic_trailing_comma 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
 
index 0b7624eaf8ae5fad9f81d7cabde094709119847f..309f22dae94b61595bdc2e9d713ee887e3b75f27 100644 (file)
@@ -5,14 +5,16 @@ chosen by the user.
 """
 
 from dataclasses import dataclass, field
-from enum import Enum
-from typing import Dict, Set
+from enum import Enum, auto
+from hashlib import sha256
+from operator import attrgetter
+from typing import Dict, Final, Set
+from warnings import warn
 
 from black.const import DEFAULT_LINE_LENGTH
 
 
 class TargetVersion(Enum):
-    PY27 = 2
     PY33 = 3
     PY34 = 4
     PY35 = 5
@@ -20,14 +22,12 @@ class TargetVersion(Enum):
     PY37 = 7
     PY38 = 8
     PY39 = 9
-
-    def is_python2(self) -> bool:
-        return self is TargetVersion.PY27
+    PY310 = 10
+    PY311 = 11
+    PY312 = 12
 
 
 class Feature(Enum):
-    # All string literals are unicode
-    UNICODE_LITERALS = 1
     F_STRINGS = 2
     NUMERIC_UNDERSCORES = 3
     TRAILING_COMMA_IN_CALL = 4
@@ -39,20 +39,30 @@ class Feature(Enum):
     ASSIGNMENT_EXPRESSIONS = 8
     POS_ONLY_ARGUMENTS = 9
     RELAXED_DECORATORS = 10
+    PATTERN_MATCHING = 11
+    UNPACKING_ON_FLOW = 12
+    ANN_ASSIGN_EXTENDED_RHS = 13
+    EXCEPT_STAR = 14
+    VARIADIC_GENERICS = 15
+    DEBUG_F_STRINGS = 16
+    PARENTHESIZED_CONTEXT_MANAGERS = 17
+    TYPE_PARAMS = 18
     FORCE_OPTIONAL_PARENTHESES = 50
 
+    # __future__ flags
+    FUTURE_ANNOTATIONS = 51
+
+
+FUTURE_FLAG_TO_FEATURE: Final = {
+    "annotations": Feature.FUTURE_ANNOTATIONS,
+}
+
 
 VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
-    TargetVersion.PY27: {Feature.ASYNC_IDENTIFIERS},
-    TargetVersion.PY33: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
-    TargetVersion.PY34: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
-    TargetVersion.PY35: {
-        Feature.UNICODE_LITERALS,
-        Feature.TRAILING_COMMA_IN_CALL,
-        Feature.ASYNC_IDENTIFIERS,
-    },
+    TargetVersion.PY33: {Feature.ASYNC_IDENTIFIERS},
+    TargetVersion.PY34: {Feature.ASYNC_IDENTIFIERS},
+    TargetVersion.PY35: {Feature.TRAILING_COMMA_IN_CALL, Feature.ASYNC_IDENTIFIERS},
     TargetVersion.PY36: {
-        Feature.UNICODE_LITERALS,
         Feature.F_STRINGS,
         Feature.NUMERIC_UNDERSCORES,
         Feature.TRAILING_COMMA_IN_CALL,
@@ -60,33 +70,93 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
         Feature.ASYNC_IDENTIFIERS,
     },
     TargetVersion.PY37: {
-        Feature.UNICODE_LITERALS,
         Feature.F_STRINGS,
         Feature.NUMERIC_UNDERSCORES,
         Feature.TRAILING_COMMA_IN_CALL,
         Feature.TRAILING_COMMA_IN_DEF,
         Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
     },
     TargetVersion.PY38: {
-        Feature.UNICODE_LITERALS,
         Feature.F_STRINGS,
+        Feature.DEBUG_F_STRINGS,
         Feature.NUMERIC_UNDERSCORES,
         Feature.TRAILING_COMMA_IN_CALL,
         Feature.TRAILING_COMMA_IN_DEF,
         Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
         Feature.ASSIGNMENT_EXPRESSIONS,
         Feature.POS_ONLY_ARGUMENTS,
+        Feature.UNPACKING_ON_FLOW,
+        Feature.ANN_ASSIGN_EXTENDED_RHS,
     },
     TargetVersion.PY39: {
-        Feature.UNICODE_LITERALS,
         Feature.F_STRINGS,
+        Feature.DEBUG_F_STRINGS,
+        Feature.NUMERIC_UNDERSCORES,
+        Feature.TRAILING_COMMA_IN_CALL,
+        Feature.TRAILING_COMMA_IN_DEF,
+        Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
+        Feature.ASSIGNMENT_EXPRESSIONS,
+        Feature.RELAXED_DECORATORS,
+        Feature.POS_ONLY_ARGUMENTS,
+        Feature.UNPACKING_ON_FLOW,
+        Feature.ANN_ASSIGN_EXTENDED_RHS,
+        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
+    },
+    TargetVersion.PY310: {
+        Feature.F_STRINGS,
+        Feature.DEBUG_F_STRINGS,
+        Feature.NUMERIC_UNDERSCORES,
+        Feature.TRAILING_COMMA_IN_CALL,
+        Feature.TRAILING_COMMA_IN_DEF,
+        Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
+        Feature.ASSIGNMENT_EXPRESSIONS,
+        Feature.RELAXED_DECORATORS,
+        Feature.POS_ONLY_ARGUMENTS,
+        Feature.UNPACKING_ON_FLOW,
+        Feature.ANN_ASSIGN_EXTENDED_RHS,
+        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
+        Feature.PATTERN_MATCHING,
+    },
+    TargetVersion.PY311: {
+        Feature.F_STRINGS,
+        Feature.DEBUG_F_STRINGS,
+        Feature.NUMERIC_UNDERSCORES,
+        Feature.TRAILING_COMMA_IN_CALL,
+        Feature.TRAILING_COMMA_IN_DEF,
+        Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
+        Feature.ASSIGNMENT_EXPRESSIONS,
+        Feature.RELAXED_DECORATORS,
+        Feature.POS_ONLY_ARGUMENTS,
+        Feature.UNPACKING_ON_FLOW,
+        Feature.ANN_ASSIGN_EXTENDED_RHS,
+        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
+        Feature.PATTERN_MATCHING,
+        Feature.EXCEPT_STAR,
+        Feature.VARIADIC_GENERICS,
+    },
+    TargetVersion.PY312: {
+        Feature.F_STRINGS,
+        Feature.DEBUG_F_STRINGS,
         Feature.NUMERIC_UNDERSCORES,
         Feature.TRAILING_COMMA_IN_CALL,
         Feature.TRAILING_COMMA_IN_DEF,
         Feature.ASYNC_KEYWORDS,
+        Feature.FUTURE_ANNOTATIONS,
         Feature.ASSIGNMENT_EXPRESSIONS,
         Feature.RELAXED_DECORATORS,
         Feature.POS_ONLY_ARGUMENTS,
+        Feature.UNPACKING_ON_FLOW,
+        Feature.ANN_ASSIGN_EXTENDED_RHS,
+        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
+        Feature.PATTERN_MATCHING,
+        Feature.EXCEPT_STAR,
+        Feature.VARIADIC_GENERICS,
+        Feature.TYPE_PARAMS,
     },
 }
 
@@ -95,6 +165,37 @@ def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> b
     return all(feature in VERSION_TO_FEATURES[version] for version in target_versions)
 
 
+class Preview(Enum):
+    """Individual preview style features."""
+
+    add_trailing_comma_consistently = auto()
+    blank_line_after_nested_stub_class = auto()
+    blank_line_between_nested_and_def_stub_file = auto()
+    hex_codes_in_unicode_sequences = auto()
+    improved_async_statements_handling = auto()
+    multiline_string_handling = auto()
+    no_blank_line_before_class_docstring = auto()
+    prefer_splitting_right_hand_side_of_assignments = auto()
+    # NOTE: string_processing requires wrap_long_dict_values_in_parens
+    # for https://github.com/psf/black/issues/3117 to be fixed.
+    string_processing = auto()
+    parenthesize_conditional_expressions = auto()
+    parenthesize_long_type_hints = auto()
+    respect_magic_trailing_comma_in_return_type = auto()
+    skip_magic_trailing_comma_in_subscript = auto()
+    wrap_long_dict_values_in_parens = auto()
+    wrap_multiple_context_managers_in_parens = auto()
+    dummy_implementations = auto()
+    walrus_subscript = auto()
+    module_docstring_newlines = auto()
+    accept_raw_docstrings = auto()
+    fix_power_op_line_length = auto()
+
+
+class Deprecated(UserWarning):
+    """Visible deprecation warning."""
+
+
 @dataclass
 class Mode:
     target_versions: Set[TargetVersion] = field(default_factory=set)
@@ -102,14 +203,36 @@ class Mode:
     string_normalization: bool = True
     is_pyi: bool = False
     is_ipynb: bool = False
+    skip_source_first_line: bool = False
     magic_trailing_comma: bool = True
     experimental_string_processing: bool = False
+    python_cell_magics: Set[str] = field(default_factory=set)
+    preview: bool = False
+
+    def __post_init__(self) -> None:
+        if self.experimental_string_processing:
+            warn(
+                "`experimental string processing` has been included in `preview`"
+                " and deprecated. Use `preview` instead.",
+                Deprecated,
+            )
+
+    def __contains__(self, feature: Preview) -> bool:
+        """
+        Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag.
+
+        The argument is not checked and features are not differentiated.
+        They only exist to make development easier by clarifying intent.
+        """
+        if feature is Preview.string_processing:
+            return self.preview or self.experimental_string_processing
+        return self.preview
 
     def get_cache_key(self) -> str:
         if self.target_versions:
             version_str = ",".join(
                 str(version.value)
-                for version in sorted(self.target_versions, key=lambda v: v.value)
+                for version in sorted(self.target_versions, key=attrgetter("value"))
             )
         else:
             version_str = "-"
@@ -119,7 +242,10 @@ class Mode:
             str(int(self.string_normalization)),
             str(int(self.is_pyi)),
             str(int(self.is_ipynb)),
+            str(int(self.skip_source_first_line)),
             str(int(self.magic_trailing_comma)),
             str(int(self.experimental_string_processing)),
+            str(int(self.preview)),
+            sha256((",".join(sorted(self.python_cell_magics))).encode()).hexdigest(),
         ]
         return ".".join(parts)
index 8f2e15b2cc3013926bc0636c05995749645f8b20..edd201a21e9f2311d3482c1183f1d100e1f073ef 100644 (file)
@@ -3,34 +3,24 @@ blib2to3 Node/Leaf transformation-related utility functions.
 """
 
 import sys
-from typing import (
-    Collection,
-    Generic,
-    Iterator,
-    List,
-    Optional,
-    Set,
-    Tuple,
-    TypeVar,
-    Union,
-)
-
-if sys.version_info < (3, 8):
-    from typing_extensions import Final
+from typing import Final, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union
+
+if sys.version_info >= (3, 10):
+    from typing import TypeGuard
 else:
-    from typing import Final
+    from typing_extensions import TypeGuard
 
-# lib2to3 fork
-from blib2to3.pytree import Node, Leaf, type_repr
-from blib2to3 import pygram
-from blib2to3.pgen2 import token
+from mypy_extensions import mypyc_attr
 
 from black.cache import CACHE_DIR
+from black.mode import Mode, Preview
 from black.strings import has_triple_quotes
-
+from blib2to3 import pygram
+from blib2to3.pgen2 import token
+from blib2to3.pytree import NL, Leaf, Node, type_repr
 
 pygram.initialize(CACHE_DIR)
-syms = pygram.python_symbols
+syms: Final = pygram.python_symbols
 
 
 # types
@@ -50,6 +40,8 @@ STATEMENT: Final = {
     syms.with_stmt,
     syms.funcdef,
     syms.classdef,
+    syms.match_stmt,
+    syms.case_block,
 }
 STANDALONE_COMMENT: Final = 153
 token.tok_name[STANDALONE_COMMENT] = "STANDALONE_COMMENT"
@@ -93,6 +85,8 @@ UNPACKING_PARENTS: Final = {
     syms.listmaker,
     syms.testlist_gexp,
     syms.testlist_star_expr,
+    syms.subject_expr,
+    syms.pattern,
 }
 TEST_DESCENDANTS: Final = {
     syms.test,
@@ -111,6 +105,7 @@ TEST_DESCENDANTS: Final = {
     syms.term,
     syms.power,
 }
+TYPED_NAMES: Final = {syms.tname, syms.tname_star}
 ASSIGNMENTS: Final = {
     "=",
     "+=",
@@ -128,16 +123,21 @@ ASSIGNMENTS: Final = {
     "//=",
 }
 
-IMPLICIT_TUPLE = {syms.testlist, syms.testlist_star_expr, syms.exprlist}
-BRACKET = {token.LPAR: token.RPAR, token.LSQB: token.RSQB, token.LBRACE: token.RBRACE}
-OPENING_BRACKETS = set(BRACKET.keys())
-CLOSING_BRACKETS = set(BRACKET.values())
-BRACKETS = OPENING_BRACKETS | CLOSING_BRACKETS
-ALWAYS_NO_SPACE = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT}
+IMPLICIT_TUPLE: Final = {syms.testlist, syms.testlist_star_expr, syms.exprlist}
+BRACKET: Final = {
+    token.LPAR: token.RPAR,
+    token.LSQB: token.RSQB,
+    token.LBRACE: token.RBRACE,
+}
+OPENING_BRACKETS: Final = set(BRACKET.keys())
+CLOSING_BRACKETS: Final = set(BRACKET.values())
+BRACKETS: Final = OPENING_BRACKETS | CLOSING_BRACKETS
+ALWAYS_NO_SPACE: Final = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT}
 
 RARROW = 55
 
 
+@mypyc_attr(allow_interpreted_subclasses=True)
 class Visitor(Generic[T]):
     """Basic lib2to3 visitor that yields things of type `T` on `visit()`."""
 
@@ -172,15 +172,15 @@ class Visitor(Generic[T]):
                 yield from self.visit(child)
 
 
-def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
+def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str:  # noqa: C901
     """Return whitespace prefix if needed for the given `leaf`.
 
     `complex_subscript` signals whether the given leaf is part of a subscription
     which has non-trivial arguments, like arithmetic expressions or function calls.
     """
-    NO = ""
-    SPACE = " "
-    DOUBLESPACE = "  "
+    NO: Final[str] = ""
+    SPACE: Final[str] = " "
+    DOUBLESPACE: Final[str] = "  "
     t = leaf.type
     p = leaf.parent
     v = leaf.value
@@ -229,6 +229,14 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
                     # that, too.
                     return prevp.prefix
 
+        elif (
+            prevp.type == token.STAR
+            and parent_type(prevp) == syms.star_expr
+            and parent_type(prevp.parent) == syms.subscriptlist
+        ):
+            # No space between typevar tuples.
+            return NO
+
         elif prevp.type in VARARGS_SPECIALS:
             if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS):
                 return NO
@@ -244,16 +252,6 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
         ):
             return NO
 
-        elif (
-            prevp.type == token.RIGHTSHIFT
-            and prevp.parent
-            and prevp.parent.type == syms.shift_expr
-            and prevp.prev_sibling
-            and prevp.prev_sibling.type == token.NAME
-            and prevp.prev_sibling.value == "print"  # type: ignore
-        ):
-            # Python 2 print chevron
-            return NO
         elif prevp.type == token.AT and p.parent and p.parent.type == syms.decorator:
             # no space in decorators
             return NO
@@ -277,7 +275,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
             return NO
 
         if t == token.EQUAL:
-            if prev.type != syms.tname:
+            if prev.type not in TYPED_NAMES:
                 return NO
 
         elif prev.type == token.EQUAL:
@@ -288,7 +286,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
         elif prev.type != token.COMMA:
             return NO
 
-    elif p.type == syms.tname:
+    elif p.type in TYPED_NAMES:
         # type names
         if not prev:
             prevp = preceding_leaf(p)
@@ -301,12 +299,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
             return NO
 
         if not prev:
-            if t == token.DOT:
-                prevp = preceding_leaf(p)
-                if not prevp or prevp.type != token.NUMBER:
-                    return NO
-
-            elif t == token.LSQB:
+            if t == token.DOT or t == token.LSQB:
                 return NO
 
         elif prev.type != token.COMMA:
@@ -353,6 +346,11 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
 
             return NO
 
+        elif Preview.walrus_subscript in mode and (
+            t == token.COLONEQUAL or prev.type == token.COLONEQUAL
+        ):
+            return SPACE
+
         elif not complex_subscript:
             return NO
 
@@ -402,6 +400,10 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
     elif p.type == syms.sliceop:
         return NO
 
+    elif p.type == syms.except_clause:
+        if t == token.STAR:
+            return NO
+
     return SPACE
 
 
@@ -439,27 +441,6 @@ def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> b
     return prev_siblings_are(node.prev_sibling, tokens[:-1])
 
 
-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 parent_type(node: Optional[LN]) -> Optional[NodeType]:
     """
     Returns:
@@ -523,23 +504,24 @@ def container_of(leaf: Leaf) -> LN:
     return container
 
 
-def first_leaf_column(node: Node) -> Optional[int]:
-    """Returns the column of the first leaf child of a node."""
-    for child in node.children:
-        if isinstance(child, Leaf):
-            return child.column
-    return None
+def first_leaf_of(node: LN) -> Optional[Leaf]:
+    """Returns the first leaf of the node tree."""
+    if isinstance(node, Leaf):
+        return node
+    if node.children:
+        return first_leaf_of(node.children[0])
+    else:
+        return None
 
 
-def first_child_is_arith(node: Node) -> bool:
-    """Whether first child is an arithmetic or a binary arithmetic expression"""
-    expr_types = {
+def is_arith_like(node: LN) -> bool:
+    """Whether node is an arithmetic or a binary arithmetic expression"""
+    return node.type in {
         syms.arith_expr,
         syms.shift_expr,
         syms.xor_expr,
         syms.and_expr,
     }
-    return bool(node.children and node.children[0].type in expr_types)
 
 
 def is_docstring(leaf: Leaf) -> bool:
@@ -583,9 +565,25 @@ def is_one_tuple(node: LN) -> bool:
     )
 
 
-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:
+def is_tuple_containing_walrus(node: LN) -> bool:
+    """Return True if `node` holds a tuple that contains a walrus operator."""
+    if node.type != syms.atom:
+        return False
+    gexp = unwrap_singleton_parenthesis(node)
+    if gexp is None or gexp.type != syms.testlist_gexp:
+        return False
+
+    return any(child.type == syms.namedexpr_test for child in gexp.children)
+
+
+def is_one_sequence_between(
+    opening: Leaf,
+    closing: Leaf,
+    leaves: List[Leaf],
+    brackets: Tuple[int, int] = (token.LPAR, token.RPAR),
+) -> bool:
+    """Return True if content between `opening` and `closing` is a one-sequence."""
+    if (opening.type, closing.type) != brackets:
         return False
 
     depth = closing.bracket_depth + 1
@@ -676,7 +674,7 @@ def is_yield(node: LN) -> bool:
     if node.type == syms.yield_expr:
         return True
 
-    if node.type == token.NAME and node.value == "yield":  # type: ignore
+    if is_name_token(node) and node.value == "yield":
         return True
 
     if node.type != syms.atom:
@@ -722,6 +720,11 @@ def is_multiline_string(leaf: Leaf) -> bool:
 
 def is_stub_suite(node: Node) -> bool:
     """Return True if `node` is a suite with a stub body."""
+
+    # If there is a comment, we want to keep it.
+    if node.prefix.strip():
+        return False
+
     if (
         len(node.children) != 4
         or node.children[0].type != token.NEWLINE
@@ -730,6 +733,9 @@ def is_stub_suite(node: Node) -> bool:
     ):
         return False
 
+    if node.children[3].prefix.strip():
+        return False
+
     return is_stub_body(node.children[2])
 
 
@@ -743,7 +749,8 @@ def is_stub_body(node: LN) -> bool:
 
     child = node.children[0]
     return (
-        child.type == syms.atom
+        not child.prefix.strip()
+        and child.type == syms.atom
         and len(child.children) == 3
         and all(leaf == Leaf(token.DOT, ".") for leaf in child.children)
     )
@@ -793,12 +800,54 @@ def is_import(leaf: Leaf) -> bool:
     )
 
 
-def is_type_comment(leaf: Leaf, suffix: str = "") -> bool:
-    """Return True if the given leaf is a special comment.
-    Only returns true for type comments for now."""
+def is_with_or_async_with_stmt(leaf: Leaf) -> bool:
+    """Return True if the given leaf starts a with or async with statement."""
+    return bool(
+        leaf.type == token.NAME
+        and leaf.value == "with"
+        and leaf.parent
+        and leaf.parent.type == syms.with_stmt
+    ) or bool(
+        leaf.type == token.ASYNC
+        and leaf.next_sibling
+        and leaf.next_sibling.type == syms.with_stmt
+    )
+
+
+def is_async_stmt_or_funcdef(leaf: Leaf) -> bool:
+    """Return True if the given leaf starts an async def/for/with statement.
+
+    Note that `async def` can be either an `async_stmt` or `async_funcdef`,
+    the latter is used when it has decorators.
+    """
+    return bool(
+        leaf.type == token.ASYNC
+        and leaf.parent
+        and leaf.parent.type in {syms.async_stmt, syms.async_funcdef}
+    )
+
+
+def is_type_comment(leaf: Leaf) -> bool:
+    """Return True if the given leaf is a type comment. This function should only
+    be used for general type comments (excluding ignore annotations, which should
+    use `is_type_ignore_comment`). Note that general type comments are no longer
+    used in modern version of Python, this function may be deprecated in the future."""
     t = leaf.type
     v = leaf.value
-    return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix)
+    return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:")
+
+
+def is_type_ignore_comment(leaf: Leaf) -> bool:
+    """Return True if the given leaf is a type comment with ignore annotation."""
+    t = leaf.type
+    v = leaf.value
+    return t in {token.COMMENT, STANDALONE_COMMENT} and is_type_ignore_comment_string(v)
+
+
+def is_type_ignore_comment_string(value: str) -> bool:
+    """Return True if the given string match with type comment with
+    ignore annotation."""
+    return value.startswith("# type: ignore")
 
 
 def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None:
@@ -843,3 +892,35 @@ def ensure_visible(leaf: Leaf) -> None:
         leaf.value = "("
     elif leaf.type == token.RPAR:
         leaf.value = ")"
+
+
+def is_name_token(nl: NL) -> TypeGuard[Leaf]:
+    return nl.type == token.NAME
+
+
+def is_lpar_token(nl: NL) -> TypeGuard[Leaf]:
+    return nl.type == token.LPAR
+
+
+def is_rpar_token(nl: NL) -> TypeGuard[Leaf]:
+    return nl.type == token.RPAR
+
+
+def is_string_token(nl: NL) -> TypeGuard[Leaf]:
+    return nl.type == token.STRING
+
+
+def is_number_token(nl: NL) -> TypeGuard[Leaf]:
+    return nl.type == token.NUMBER
+
+
+def is_part_of_annotation(leaf: Leaf) -> bool:
+    """Returns whether this leaf is part of type annotations."""
+    ancestor = leaf.parent
+    while ancestor is not None:
+        if ancestor.prev_sibling and ancestor.prev_sibling.type == token.RARROW:
+            return True
+        if ancestor.parent and ancestor.parent.type == syms.tname:
+            return True
+        ancestor = ancestor.parent
+    return False
index cb1c83e7b7896e92ba16ffc266d0e531a41cea2a..67ac8595fcc6783ea2c6bf9de96d0013044ae496 100644 (file)
@@ -1,6 +1,7 @@
 """
 Formatting numeric literals.
 """
+
 from blib2to3.pytree import Leaf
 
 
@@ -25,13 +26,10 @@ def format_scientific_notation(text: str) -> str:
     return f"{before}e{sign}{after}"
 
 
-def format_long_or_complex_number(text: str) -> str:
-    """Formats a long or complex string like `10L` or `10j`"""
+def format_complex_number(text: str) -> str:
+    """Formats a complex string like `10j`"""
     number = text[:-1]
     suffix = text[-1]
-    # Capitalize in "2L" because "l" looks too similar to "1".
-    if suffix == "l":
-        suffix = "L"
     return f"{format_float_or_int_string(number)}{suffix}"
 
 
@@ -47,9 +45,7 @@ def format_float_or_int_string(text: str) -> str:
 def normalize_numeric_literal(leaf: Leaf) -> None:
     """Normalizes numeric (float, int, and complex) literals.
 
-    All letters used in the representation are normalized to lowercase (except
-    in Python 2 long literals).
-    """
+    All letters used in the representation are normalized to lowercase."""
     text = leaf.value.lower()
     if text.startswith(("0o", "0b")):
         # Leave octal and binary literals alone.
@@ -58,8 +54,8 @@ def normalize_numeric_literal(leaf: Leaf) -> None:
         text = format_hex(text)
     elif "e" in text:
         text = format_scientific_notation(text)
-    elif text.endswith(("j", "l")):
-        text = format_long_or_complex_number(text)
+    elif text.endswith("j"):
+        text = format_complex_number(text)
     else:
         text = format_float_or_int_string(text)
     leaf.value = text
index fd3dbb376279497c2fc23f6eb7f03d6db96c4fc7..f4c17f28ea418d363f9b388e3770c208ca26b1b1 100644 (file)
@@ -4,13 +4,14 @@ The double calls are for patching purposes in tests.
 """
 
 import json
-from typing import Any, Optional
-from mypy_extensions import mypyc_attr
 import tempfile
+from typing import Any, Optional
 
 from click import echo, style
+from mypy_extensions import mypyc_attr
 
 
+@mypyc_attr(patchable=True)
 def _out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
     if message is not None:
         if "bold" not in styles:
@@ -19,6 +20,7 @@ def _out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
     echo(message, nl=nl, err=True)
 
 
+@mypyc_attr(patchable=True)
 def _err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
     if message is not None:
         if "fg" not in styles:
@@ -27,6 +29,7 @@ def _err(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
     echo(message, nl=nl, err=True)
 
 
+@mypyc_attr(patchable=True)
 def out(message: Optional[str] = None, nl: bool = True, **styles: Any) -> None:
     _out(message, nl=nl, **styles)
 
@@ -56,8 +59,8 @@ def diff(a: str, b: str, a_name: str, b_name: str) -> str:
     """Return a unified diff string between strings `a` and `b`."""
     import difflib
 
-    a_lines = [line for line in a.splitlines(keepends=True)]
-    b_lines = [line for line in b.splitlines(keepends=True)]
+    a_lines = a.splitlines(keepends=True)
+    b_lines = b.splitlines(keepends=True)
     diff_lines = []
     for line in difflib.unified_diff(
         a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5
@@ -78,7 +81,7 @@ def color_diff(contents: str) -> str:
     lines = contents.split("\n")
     for i, line in enumerate(lines):
         if line.startswith("+++") or line.startswith("---"):
-            line = "\033[1;37m" + line + "\033[0m"  # bold white, reset
+            line = "\033[1m" + line + "\033[0m"  # bold, reset
         elif line.startswith("@@"):
             line = "\033[36m" + line + "\033[0m"  # cyan, reset
         elif line.startswith("+"):
index 0b8d984cedd2a43dcf6e80ab59a9041e481588f0..ea282d1805cfea68ccf406e7e92e676de16f5fca 100644 (file)
@@ -1,33 +1,19 @@
 """
 Parse Python code and perform AST validation.
 """
+
 import ast
 import sys
-from typing import Iterable, Iterator, List, Set, Union, Tuple
+from typing import Iterable, Iterator, List, Set, Tuple
 
-# lib2to3 fork
-from blib2to3.pytree import Node, Leaf
-from blib2to3 import pygram, pytree
+from black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature
+from black.nodes import syms
+from blib2to3 import pygram
 from blib2to3.pgen2 import driver
 from blib2to3.pgen2.grammar import Grammar
 from blib2to3.pgen2.parse import ParseError
-
-from black.mode import TargetVersion, Feature, supports_feature
-from black.nodes import syms
-
-try:
-    from typed_ast import ast3, ast27
-except ImportError:
-    if sys.version_info < (3, 8):
-        print(
-            "The typed_ast package is required but not installed.\n"
-            "You can upgrade to Python 3.8+ or install typed_ast with\n"
-            "`python3 -m pip install typed-ast`.",
-            file=sys.stderr,
-        )
-        sys.exit(1)
-    else:
-        ast3 = ast27 = ast
+from blib2to3.pgen2.tokenize import TokenError
+from blib2to3.pytree import Leaf, Node
 
 
 class InvalidInput(ValueError):
@@ -38,36 +24,28 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
     if not target_versions:
         # No target_version specified, so try all grammars.
         return [
-            # Python 3.7+
-            pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords,
+            # Python 3.7-3.9
+            pygram.python_grammar_async_keywords,
             # Python 3.0-3.6
-            pygram.python_grammar_no_print_statement_no_exec_statement,
-            # Python 2.7 with future print_function import
-            pygram.python_grammar_no_print_statement,
-            # Python 2.7
-            pygram.python_grammar,
-        ]
-
-    if all(version.is_python2() for version in target_versions):
-        # Python 2-only code, so try Python 2 grammars.
-        return [
-            # Python 2.7 with future print_function import
-            pygram.python_grammar_no_print_statement,
-            # Python 2.7
             pygram.python_grammar,
+            # Python 3.10+
+            pygram.python_grammar_soft_keywords,
         ]
 
-    # Python 3-compatible code, so only try Python 3 grammar.
     grammars = []
     # If we have to parse both, try to parse async as a keyword first
-    if not supports_feature(target_versions, Feature.ASYNC_IDENTIFIERS):
-        # Python 3.7+
-        grammars.append(
-            pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords
-        )
+    if not supports_feature(
+        target_versions, Feature.ASYNC_IDENTIFIERS
+    ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING):
+        # Python 3.7-3.9
+        grammars.append(pygram.python_grammar_async_keywords)
     if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
         # Python 3.0-3.6
-        grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement)
+        grammars.append(pygram.python_grammar)
+    if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions):
+        # Python 3.10+
+        grammars.append(pygram.python_grammar_soft_keywords)
+
     # At least one of the above branches must have been taken, because every Python
     # version has exactly one of the two 'ASYNC_*' flags
     return grammars
@@ -78,8 +56,10 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -
     if not src_txt.endswith("\n"):
         src_txt += "\n"
 
-    for grammar in get_grammars(set(target_versions)):
-        drv = driver.Driver(grammar, pytree.convert)
+    grammars = get_grammars(set(target_versions))
+    errors = {}
+    for grammar in grammars:
+        drv = driver.Driver(grammar)
         try:
             result = drv.parse_string(src_txt, True)
             break
@@ -91,8 +71,21 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -
                 faulty_line = lines[lineno - 1]
             except IndexError:
                 faulty_line = "<line number missing in source>"
-            exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}")
+            errors[grammar.version] = InvalidInput(
+                f"Cannot parse: {lineno}:{column}: {faulty_line}"
+            )
+
+        except TokenError as te:
+            # In edge cases these are raised; and typically don't have a "faulty_line".
+            lineno, column = te.args[1]
+            errors[grammar.version] = InvalidInput(
+                f"Cannot parse: {lineno}:{column}: {te.args[0]}"
+            )
+
     else:
+        # Choose the latest version when raising the actual parsing error.
+        assert len(errors) >= 1
+        exc = errors[max(errors)]
         raise exc from None
 
     if isinstance(result, Leaf):
@@ -100,6 +93,16 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -
     return result
 
 
+def matches_grammar(src_txt: str, grammar: Grammar) -> bool:
+    drv = driver.Driver(grammar)
+    try:
+        drv.parse_string(src_txt, True)
+    except (ParseError, TokenError, IndentationError):
+        return False
+    else:
+        return True
+
+
 def lib2to3_unparse(node: Node) -> str:
     """Given a lib2to3 node, return its string representation."""
     code = str(node)
@@ -107,56 +110,68 @@ def lib2to3_unparse(node: Node) -> str:
 
 
 def parse_single_version(
-    src: str, version: Tuple[int, int]
-) -> Union[ast.AST, ast3.AST, ast27.AST]:
+    src: str, version: Tuple[int, int], *, type_comments: bool
+) -> ast.AST:
     filename = "<unknown>"
-    # typed_ast is needed because of feature version limitations in the builtin ast
-    if sys.version_info >= (3, 8) and version >= (3,):
-        return ast.parse(src, filename, feature_version=version)
-    elif version >= (3,):
-        return ast3.parse(src, filename, feature_version=version[1])
-    elif version == (2, 7):
-        return ast27.parse(src)
-    raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!")
+    return ast.parse(
+        src, filename, feature_version=version, type_comments=type_comments
+    )
 
 
-def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]:
+def parse_ast(src: str) -> ast.AST:
     # TODO: support Python 4+ ;)
     versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]
 
-    if ast27.__name__ != "ast":
-        versions.append((2, 7))
-
     first_error = ""
     for version in sorted(versions, reverse=True):
         try:
-            return parse_single_version(src, version)
+            return parse_single_version(src, version, type_comments=True)
         except SyntaxError as e:
             if not first_error:
                 first_error = str(e)
 
+    # Try to parse without type comments
+    for version in sorted(versions, reverse=True):
+        try:
+            return parse_single_version(src, version, type_comments=False)
+        except SyntaxError:
+            pass
+
     raise SyntaxError(first_error)
 
 
-def stringify_ast(
-    node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0
-) -> Iterator[str]:
+def _normalize(lineend: str, value: str) -> str:
+    # To normalize, we strip any leading and trailing space from
+    # each line...
+    stripped: List[str] = [i.strip() for i in value.splitlines()]
+    normalized = lineend.join(stripped)
+    # ...and remove any blank lines at the beginning and end of
+    # the whole string
+    return normalized.strip()
+
+
+def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]:
     """Simple visitor generating strings to compare ASTs by content."""
 
-    node = fixup_ast_constants(node)
+    if (
+        isinstance(node, ast.Constant)
+        and isinstance(node.value, str)
+        and node.kind == "u"
+    ):
+        # It's a quirk of history that we strip the u prefix over here. We used to
+        # rewrite the AST nodes for Python version compatibility and we never copied
+        # over the kind
+        node.kind = None
 
     yield f"{'  ' * depth}{node.__class__.__name__}("
 
     for field in sorted(node._fields):  # noqa: F402
         # TypeIgnore has only one field 'lineno' which breaks this comparison
-        type_ignore_classes = (ast3.TypeIgnore, ast27.TypeIgnore)
-        if sys.version_info >= (3, 8):
-            type_ignore_classes += (ast.TypeIgnore,)
-        if isinstance(node, type_ignore_classes):
+        if isinstance(node, ast.TypeIgnore):
             break
 
         try:
-            value = getattr(node, field)
+            value: object = getattr(node, field)
         except AttributeError:
             continue
 
@@ -168,56 +183,34 @@ def stringify_ast(
                 # parentheses and they change the AST.
                 if (
                     field == "targets"
-                    and isinstance(node, (ast.Delete, ast3.Delete, ast27.Delete))
-                    and isinstance(item, (ast.Tuple, ast3.Tuple, ast27.Tuple))
+                    and isinstance(node, ast.Delete)
+                    and isinstance(item, ast.Tuple)
                 ):
-                    for item in item.elts:
-                        yield from stringify_ast(item, depth + 2)
+                    for elt in item.elts:
+                        yield from stringify_ast(elt, depth + 2)
 
-                elif isinstance(item, (ast.AST, ast3.AST, ast27.AST)):
+                elif isinstance(item, ast.AST):
                     yield from stringify_ast(item, depth + 2)
 
-        elif isinstance(value, (ast.AST, ast3.AST, ast27.AST)):
+        elif isinstance(value, ast.AST):
             yield from stringify_ast(value, depth + 2)
 
         else:
-            # Constant strings may be indented across newlines, if they are
-            # docstrings; fold spaces after newlines when comparing. Similarly,
-            # trailing and leading space may be removed.
-            # Note that when formatting Python 2 code, at least with Windows
-            # line-endings, docstrings can end up here as bytes instead of
-            # str so make sure that we handle both cases.
+            normalized: object
             if (
                 isinstance(node, ast.Constant)
                 and field == "value"
-                and isinstance(value, (str, bytes))
+                and isinstance(value, str)
             ):
-                lineend = "\n" if isinstance(value, str) else b"\n"
-                # To normalize, we strip any leading and trailing space from
-                # each line...
-                stripped = [line.strip() for line in value.splitlines()]
-                normalized = lineend.join(stripped)  # type: ignore[attr-defined]
-                # ...and remove any blank lines at the beginning and end of
-                # the whole string
-                normalized = normalized.strip()
+                # Constant strings may be indented across newlines, if they are
+                # docstrings; fold spaces after newlines when comparing. Similarly,
+                # trailing and leading space may be removed.
+                normalized = _normalize("\n", value)
+            elif field == "type_comment" and isinstance(value, str):
+                # Trailing whitespace in type comments is removed.
+                normalized = value.rstrip()
             else:
                 normalized = value
             yield f"{'  ' * (depth+2)}{normalized!r},  # {value.__class__.__name__}"
 
     yield f"{'  ' * depth})  # /{node.__class__.__name__}"
-
-
-def fixup_ast_constants(
-    node: Union[ast.AST, ast3.AST, ast27.AST]
-) -> Union[ast.AST, ast3.AST, ast27.AST]:
-    """Map ast nodes deprecated in 3.8 to Constant."""
-    if isinstance(node, (ast.Str, ast3.Str, ast27.Str, ast.Bytes, ast3.Bytes)):
-        return ast.Constant(value=node.s)
-
-    if isinstance(node, (ast.Num, ast3.Num, ast27.Num)):
-        return ast.Constant(value=node.n)
-
-    if isinstance(node, (ast.NameConstant, ast3.NameConstant)):
-        return ast.Constant(value=node.value)
-
-    return node
index 7e1c8b4b87f273bf6b99868fcdc641afadb8c4d6..89899f2f38996f309f79dc1225e24bfa8c69767a 100644 (file)
@@ -1,13 +1,14 @@
 """
 Summarize Black runs to users.
 """
+
 from dataclasses import dataclass
 from enum import Enum
 from pathlib import Path
 
 from click import style
 
-from black.output import out, err
+from black.output import err, out
 
 
 class Changed(Enum):
@@ -93,11 +94,13 @@ class Report:
         if self.change_count:
             s = "s" if self.change_count > 1 else ""
             report.append(
-                style(f"{self.change_count} file{s} {reformatted}", bold=True)
+                style(f"{self.change_count} file{s} ", bold=True, fg="blue")
+                + style(f"{reformatted}", bold=True)
             )
+
         if self.same_count:
             s = "s" if self.same_count > 1 else ""
-            report.append(f"{self.same_count} file{s} {unchanged}")
+            report.append(style(f"{self.same_count} file{s} ", fg="blue") + unchanged)
         if self.failure_count:
             s = "s" if self.failure_count > 1 else ""
             report.append(style(f"{self.failure_count} file{s} {failed}", fg="red"))
index 822e3d7858a077425bc83ade95fe74aa2eac2d59..ebd4c052d1f37b9eed01d329143c15bfbe0f0e74 100644 (file)
@@ -2,8 +2,8 @@
 
 See https://doc.rust-lang.org/book/ch09-00-error-handling.html.
 """
-from typing import Generic, TypeVar, Union
 
+from typing import Generic, TypeVar, Union
 
 T = TypeVar("T")
 E = TypeVar("E", bound=Exception)
index d7b6c240e80215ef1c268634433074d82e4c5aeb..0d30f09ed112d28583c9579f82e6221087b5bb01 100644 (file)
@@ -2,12 +2,28 @@
 Simple formatting on strings. Further string formatting code is in trans.py.
 """
 
-import regex as re
+import re
 import sys
-from typing import List, Pattern
-
-
-STRING_PREFIX_CHARS = "furbFURB"  # All possible string prefix characters.
+from functools import lru_cache
+from typing import Final, List, Match, Pattern
+
+from black._width_table import WIDTH_TABLE
+from blib2to3.pytree import Leaf
+
+STRING_PREFIX_CHARS: Final = "furbFURB"  # All possible string prefix characters.
+STRING_PREFIX_RE: Final = re.compile(
+    r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", re.DOTALL
+)
+FIRST_NON_WHITESPACE_RE: Final = re.compile(r"\s*\t+\s*(\S)")
+UNICODE_ESCAPE_RE: Final = re.compile(
+    r"(?P<backslashes>\\+)(?P<body>"
+    r"(u(?P<u>[a-fA-F0-9]{4}))"  # Character with 16-bit hex value xxxx
+    r"|(U(?P<U>[a-fA-F0-9]{8}))"  # Character with 32-bit hex value xxxxxxxx
+    r"|(x(?P<x>[a-fA-F0-9]{2}))"  # Character with hex value hh
+    r"|(N\{(?P<N>[a-zA-Z0-9 \-]{2,})\})"  # Character named name in the Unicode database
+    r")",
+    re.VERBOSE,
+)
 
 
 def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str:
@@ -37,7 +53,7 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]:
     for line in s.splitlines():
         # Find the index of the first non-whitespace character after a string of
         # whitespace that includes at least one tab
-        match = re.match(r"\s*\t+\s*(\S)", line)
+        match = FIRST_NON_WHITESPACE_RE.match(line)
         if match:
             first_non_whitespace_idx = match.start(1)
 
@@ -128,20 +144,32 @@ def assert_is_leaf_string(string: str) -> None:
     ), f"{set(string[:quote_idx])} is NOT a subset of {set(STRING_PREFIX_CHARS)}."
 
 
-def normalize_string_prefix(s: str, remove_u_prefix: bool = False) -> str:
-    """Make all string prefixes lowercase.
-
-    If remove_u_prefix is given, also removes any u prefix from the string.
-    """
-    match = re.match(r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", s, re.DOTALL)
+def normalize_string_prefix(s: str) -> str:
+    """Make all string prefixes lowercase."""
+    match = STRING_PREFIX_RE.match(s)
     assert match is not None, f"failed to match string {s!r}"
     orig_prefix = match.group(1)
-    new_prefix = orig_prefix.replace("F", "f").replace("B", "b").replace("U", "u")
-    if remove_u_prefix:
-        new_prefix = new_prefix.replace("u", "")
+    new_prefix = (
+        orig_prefix.replace("F", "f")
+        .replace("B", "b")
+        .replace("U", "")
+        .replace("u", "")
+    )
+
+    # Python syntax guarantees max 2 prefixes and that one of them is "r"
+    if len(new_prefix) == 2 and "r" != new_prefix[0].lower():
+        new_prefix = new_prefix[::-1]
     return f"{new_prefix}{match.group(2)}"
 
 
+# Re(gex) does actually cache patterns internally but this still improves
+# performance on a long list literal of strings by 5-9% since lru_cache's
+# caching overhead is much lower.
+@lru_cache(maxsize=64)
+def _cached_compile(pattern: str) -> Pattern[str]:
+    return re.compile(pattern)
+
+
 def normalize_string_quotes(s: str) -> str:
     """Prefer double quotes but only if it doesn't cause more escaping.
 
@@ -166,9 +194,9 @@ def normalize_string_quotes(s: str) -> str:
         return s  # There's an internal error
 
     prefix = s[:first_quote_pos]
-    unescaped_new_quote = re.compile(rf"(([^\\]|^)(\\\\)*){new_quote}")
-    escaped_new_quote = re.compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}")
-    escaped_orig_quote = re.compile(rf"([^\\]|^)\\((?:\\\\)*){orig_quote}")
+    unescaped_new_quote = _cached_compile(rf"(([^\\]|^)(\\\\)*){new_quote}")
+    escaped_new_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}")
+    escaped_orig_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){orig_quote}")
     body = s[first_quote_pos + len(orig_quote) : -len(orig_quote)]
     if "r" in prefix.casefold():
         if unescaped_new_quote.search(body):
@@ -214,3 +242,88 @@ def normalize_string_quotes(s: str) -> str:
         return s  # Prefer double quotes
 
     return f"{prefix}{new_quote}{new_body}{new_quote}"
+
+
+def normalize_unicode_escape_sequences(leaf: Leaf) -> None:
+    """Replace hex codes in Unicode escape sequences with lowercase representation."""
+    text = leaf.value
+    prefix = get_string_prefix(text)
+    if "r" in prefix.lower():
+        return
+
+    def replace(m: Match[str]) -> str:
+        groups = m.groupdict()
+        back_slashes = groups["backslashes"]
+
+        if len(back_slashes) % 2 == 0:
+            return back_slashes + groups["body"]
+
+        if groups["u"]:
+            # \u
+            return back_slashes + "u" + groups["u"].lower()
+        elif groups["U"]:
+            # \U
+            return back_slashes + "U" + groups["U"].lower()
+        elif groups["x"]:
+            # \x
+            return back_slashes + "x" + groups["x"].lower()
+        else:
+            assert groups["N"], f"Unexpected match: {m}"
+            # \N{}
+            return back_slashes + "N{" + groups["N"].upper() + "}"
+
+    leaf.value = re.sub(UNICODE_ESCAPE_RE, replace, text)
+
+
+@lru_cache(maxsize=4096)
+def char_width(char: str) -> int:
+    """Return the width of a single character as it would be displayed in a
+    terminal or editor (which respects Unicode East Asian Width).
+
+    Full width characters are counted as 2, while half width characters are
+    counted as 1.  Also control characters are counted as 0.
+    """
+    table = WIDTH_TABLE
+    codepoint = ord(char)
+    highest = len(table) - 1
+    lowest = 0
+    idx = highest // 2
+    while True:
+        start_codepoint, end_codepoint, width = table[idx]
+        if codepoint < start_codepoint:
+            highest = idx - 1
+        elif codepoint > end_codepoint:
+            lowest = idx + 1
+        else:
+            return 0 if width < 0 else width
+        if highest < lowest:
+            break
+        idx = (highest + lowest) // 2
+    return 1
+
+
+def str_width(line_str: str) -> int:
+    """Return the width of `line_str` as it would be displayed in a terminal
+    or editor (which respects Unicode East Asian Width).
+
+    You could utilize this function to determine, for example, if a string
+    is too wide to display in a terminal or editor.
+    """
+    if line_str.isascii():
+        # Fast path for a line consisting of only ASCII characters
+        return len(line_str)
+    return sum(map(char_width, line_str))
+
+
+def count_chars_in_width(line_str: str, max_width: int) -> int:
+    """Count the number of characters in `line_str` that would fit in a
+    terminal or editor of `max_width` (which respects Unicode East Asian
+    Width).
+    """
+    total_width = 0
+    for i, char in enumerate(line_str):
+        width = char_width(char)
+        if width + total_width > max_width:
+            return i
+        total_width += width
+    return len(line_str)
index 023dcd3618a5c0edb6b6f5e7e91e09bb6d3add30..a3f6467cc9e3920758ce3b5ddf00a5e2c41129b6 100644 (file)
@@ -1,18 +1,22 @@
 """
 String transformers that can split and merge strings.
 """
+
+import re
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from dataclasses import dataclass
-import regex as re
 from typing import (
     Any,
     Callable,
+    ClassVar,
     Collection,
     Dict,
+    Final,
     Iterable,
     Iterator,
     List,
+    Literal,
     Optional,
     Sequence,
     Set,
@@ -21,20 +25,34 @@ from typing import (
     Union,
 )
 
-from black.rusty import Result, Ok, Err
+from mypy_extensions import trait
 
-from black.mode import Feature
-from black.nodes import syms, replace_child, parent_type
-from black.nodes import is_empty_par, is_empty_lpar, is_empty_rpar
-from black.nodes import OPENING_BRACKETS, CLOSING_BRACKETS, STANDALONE_COMMENT
-from black.lines import Line, append_leaves
-from black.brackets import BracketMatchError
 from black.comments import contains_pragma_comment
-from black.strings import has_triple_quotes, get_string_prefix, assert_is_leaf_string
-from black.strings import normalize_string_quotes
-
-from blib2to3.pytree import Leaf, Node
+from black.lines import Line, append_leaves
+from black.mode import Feature, Mode
+from black.nodes import (
+    CLOSING_BRACKETS,
+    OPENING_BRACKETS,
+    STANDALONE_COMMENT,
+    is_empty_lpar,
+    is_empty_par,
+    is_empty_rpar,
+    is_part_of_annotation,
+    parent_type,
+    replace_child,
+    syms,
+)
+from black.rusty import Err, Ok, Result
+from black.strings import (
+    assert_is_leaf_string,
+    count_chars_in_width,
+    get_string_prefix,
+    has_triple_quotes,
+    normalize_string_quotes,
+    str_width,
+)
 from blib2to3.pgen2 import token
+from blib2to3.pytree import Leaf, Node
 
 
 class CannotTransform(Exception):
@@ -44,13 +62,15 @@ class CannotTransform(Exception):
 # types
 T = TypeVar("T")
 LN = Union[Leaf, Node]
-Transformer = Callable[[Line, Collection[Feature]], Iterator[Line]]
+Transformer = Callable[[Line, Collection[Feature], Mode], Iterator[Line]]
 Index = int
 NodeType = int
 ParserState = int
 StringID = int
 TResult = Result[T, CannotTransform]  # (T)ransform Result
-TMatchResult = TResult[Index]
+TMatchResult = TResult[List[Index]]
+
+SPLIT_SAFE_CHARS = frozenset(["\u3001", "\u3002", "\uff0c"])  # East Asian stops
 
 
 def TErr(err_msg: str) -> Err[CannotTransform]:
@@ -62,7 +82,86 @@ def TErr(err_msg: str) -> Err[CannotTransform]:
     return Err(cant_transform)
 
 
-@dataclass  # type: ignore
+def hug_power_op(
+    line: Line, features: Collection[Feature], mode: Mode
+) -> Iterator[Line]:
+    """A transformer which normalizes spacing around power operators."""
+
+    # Performance optimization to avoid unnecessary Leaf clones and other ops.
+    for leaf in line.leaves:
+        if leaf.type == token.DOUBLESTAR:
+            break
+    else:
+        raise CannotTransform("No doublestar token was found in the line.")
+
+    def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool:
+        # Brackets and parentheses indicate calls, subscripts, etc. ...
+        # basically stuff that doesn't count as "simple". Only a NAME lookup
+        # or dotted lookup (eg. NAME.NAME) is OK.
+        if step == -1:
+            disallowed = {token.RPAR, token.RSQB}
+        else:
+            disallowed = {token.LPAR, token.LSQB}
+
+        while 0 <= index < len(line.leaves):
+            current = line.leaves[index]
+            if current.type in disallowed:
+                return False
+            if current.type not in {token.NAME, token.DOT} or current.value == "for":
+                # If the current token isn't disallowed, we'll assume this is simple as
+                # only the disallowed tokens are semantically attached to this lookup
+                # expression we're checking. Also, stop early if we hit the 'for' bit
+                # of a comprehension.
+                return True
+
+            index += step
+
+        return True
+
+    def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool:
+        # An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple
+        # lookup (see above), with or without a preceding unary operator.
+        start = line.leaves[index]
+        if start.type in {token.NAME, token.NUMBER}:
+            return is_simple_lookup(index, step=(1 if kind == "exponent" else -1))
+
+        if start.type in {token.PLUS, token.MINUS, token.TILDE}:
+            if line.leaves[index + 1].type in {token.NAME, token.NUMBER}:
+                # step is always one as bases with a preceding unary op will be checked
+                # for simplicity starting from the next token (so it'll hit the check
+                # above).
+                return is_simple_lookup(index + 1, step=1)
+
+        return False
+
+    new_line = line.clone()
+    should_hug = False
+    for idx, leaf in enumerate(line.leaves):
+        new_leaf = leaf.clone()
+        if should_hug:
+            new_leaf.prefix = ""
+            should_hug = False
+
+        should_hug = (
+            (0 < idx < len(line.leaves) - 1)
+            and leaf.type == token.DOUBLESTAR
+            and is_simple_operand(idx - 1, kind="base")
+            and line.leaves[idx - 1].value != "lambda"
+            and is_simple_operand(idx + 1, kind="exponent")
+        )
+        if should_hug:
+            new_leaf.prefix = ""
+
+        # We have to be careful to make a new line properly:
+        # - bracket related metadata must be maintained (handled by Line.append)
+        # - comments need to copied over, updating the leaf IDs they're attached to
+        new_line.append(new_leaf, preformatted=True)
+        for comment_leaf in line.comments_after(leaf):
+            new_line.append(comment_leaf, preformatted=True)
+
+    yield new_line
+
+
 class StringTransformer(ABC):
     """
     An implementation of the Transformer protocol that relies on its
@@ -90,31 +189,40 @@ class StringTransformer(ABC):
         as much as possible.
     """
 
-    line_length: int
-    normalize_strings: bool
-    __name__ = "StringTransformer"
+    __name__: Final = "StringTransformer"
+
+    # Ideally this would be a dataclass, but unfortunately mypyc breaks when used with
+    # `abc.ABC`.
+    def __init__(self, line_length: int, normalize_strings: bool) -> None:
+        self.line_length = line_length
+        self.normalize_strings = normalize_strings
 
     @abstractmethod
     def do_match(self, line: Line) -> TMatchResult:
         """
         Returns:
-            * Ok(string_idx) such that `line.leaves[string_idx]` is our target
-            string, if a match was able to be made.
-                OR
-            * Err(CannotTransform), if a match was not able to be made.
+            * Ok(string_indices) such that for each index, `line.leaves[index]`
+              is our target string if a match was able to be made. For
+              transformers that don't result in more lines (e.g. StringMerger,
+              StringParenStripper), multiple matches and transforms are done at
+              once to reduce the complexity.
+              OR
+            * Err(CannotTransform), if no match could be made.
         """
 
     @abstractmethod
-    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+    def do_transform(
+        self, line: Line, string_indices: List[int]
+    ) -> Iterator[TResult[Line]]:
         """
         Yields:
             * Ok(new_line) where new_line is the new transformed line.
-                OR
+              OR
             * Err(CannotTransform) if the transformation failed for some reason. The
-            `do_match(...)` template method should usually be used to reject
-            the form of the given Line, but in some cases it is difficult to
-            know whether or not a Line meets the StringTransformer's
-            requirements until the transformation is already midway.
+              `do_match(...)` template method should usually be used to reject
+              the form of the given Line, but in some cases it is difficult to
+              know whether or not a Line meets the StringTransformer's
+              requirements until the transformation is already midway.
 
         Side Effects:
             This method should NOT mutate @line directly, but it MAY mutate the
@@ -123,7 +231,9 @@ class StringTransformer(ABC):
             yield an CannotTransform after that point.)
         """
 
-    def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]:
+    def __call__(
+        self, line: Line, _features: Collection[Feature], _mode: Mode
+    ) -> Iterator[Line]:
         """
         StringTransformer instances have a call signature that mirrors that of
         the Transformer type.
@@ -146,9 +256,9 @@ class StringTransformer(ABC):
                 " this line as one that it can transform."
             ) from cant_transform
 
-        string_idx = match_result.ok()
+        string_indices = match_result.ok()
 
-        for line_result in self.do_transform(line, string_idx):
+        for line_result in self.do_transform(line, string_indices):
             if isinstance(line_result, Err):
                 cant_transform = line_result.err()
                 raise CannotTransform(
@@ -184,6 +294,7 @@ class CustomSplit:
     break_idx: int
 
 
+@trait
 class CustomSplitMapMixin:
     """
     This mixin class is used to map merged strings to a sequence of
@@ -191,8 +302,10 @@ class CustomSplitMapMixin:
     the resultant substrings go over the configured max line length.
     """
 
-    _Key = Tuple[StringID, str]
-    _CUSTOM_SPLIT_MAP: Dict[_Key, Tuple[CustomSplit, ...]] = defaultdict(tuple)
+    _Key: ClassVar = Tuple[StringID, str]
+    _CUSTOM_SPLIT_MAP: ClassVar[Dict[_Key, Tuple[CustomSplit, ...]]] = defaultdict(
+        tuple
+    )
 
     @staticmethod
     def _get_key(string: str) -> "CustomSplitMapMixin._Key":
@@ -219,8 +332,8 @@ class CustomSplitMapMixin:
 
         Returns:
             * A list of the custom splits that are mapped to @string, if any
-            exist.
-                OR
+              exist.
+              OR
             * [], otherwise.
 
         Side Effects:
@@ -243,20 +356,20 @@ class CustomSplitMapMixin:
         return key in self._CUSTOM_SPLIT_MAP
 
 
-class StringMerger(CustomSplitMapMixin, StringTransformer):
+class StringMerger(StringTransformer, CustomSplitMapMixin):
     """StringTransformer that merges strings together.
 
     Requirements:
         (A) The line contains adjacent strings such that ALL of the validation checks
-        listed in StringMerger.__validate_msg(...)'s docstring pass.
-            OR
+        listed in StringMerger._validate_msg(...)'s docstring pass.
+        OR
         (B) The line contains a string which uses line continuation backslashes.
 
     Transformations:
         Depending on which of the two requirements above where met, either:
 
         (A) The string group associated with the target string is merged.
-            OR
+        OR
         (B) All line-continuation backslashes are removed from the target string.
 
     Collaborations:
@@ -268,28 +381,62 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
 
         is_valid_index = is_valid_index_factory(LL)
 
-        for (i, leaf) in enumerate(LL):
+        string_indices = []
+        idx = 0
+        while is_valid_index(idx):
+            leaf = LL[idx]
             if (
                 leaf.type == token.STRING
-                and is_valid_index(i + 1)
-                and LL[i + 1].type == token.STRING
+                and is_valid_index(idx + 1)
+                and LL[idx + 1].type == token.STRING
             ):
-                return Ok(i)
+                # Let's check if the string group contains an inline comment
+                # If we have a comment inline, we don't merge the strings
+                contains_comment = False
+                i = idx
+                while is_valid_index(i):
+                    if LL[i].type != token.STRING:
+                        break
+                    if line.comments_after(LL[i]):
+                        contains_comment = True
+                        break
+                    i += 1
+
+                if not is_part_of_annotation(leaf) and not contains_comment:
+                    string_indices.append(idx)
 
-            if leaf.type == token.STRING and "\\\n" in leaf.value:
-                return Ok(i)
+                # Advance to the next non-STRING leaf.
+                idx += 2
+                while is_valid_index(idx) and LL[idx].type == token.STRING:
+                    idx += 1
 
-        return TErr("This line has no strings that need merging.")
+            elif leaf.type == token.STRING and "\\\n" in leaf.value:
+                string_indices.append(idx)
+                # Advance to the next non-STRING leaf.
+                idx += 1
+                while is_valid_index(idx) and LL[idx].type == token.STRING:
+                    idx += 1
 
-    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+            else:
+                idx += 1
+
+        if string_indices:
+            return Ok(string_indices)
+        else:
+            return TErr("This line has no strings that need merging.")
+
+    def do_transform(
+        self, line: Line, string_indices: List[int]
+    ) -> Iterator[TResult[Line]]:
         new_line = line
+
         rblc_result = self._remove_backslash_line_continuation_chars(
-            new_line, string_idx
+            new_line, string_indices
         )
         if isinstance(rblc_result, Ok):
             new_line = rblc_result.ok()
 
-        msg_result = self._merge_string_group(new_line, string_idx)
+        msg_result = self._merge_string_group(new_line, string_indices)
         if isinstance(msg_result, Ok):
             new_line = msg_result.ok()
 
@@ -310,7 +457,7 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
 
     @staticmethod
     def _remove_backslash_line_continuation_chars(
-        line: Line, string_idx: int
+        line: Line, string_indices: List[int]
     ) -> TResult[Line]:
         """
         Merge strings that were split across multiple lines using
@@ -324,34 +471,44 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
         """
         LL = line.leaves
 
-        string_leaf = LL[string_idx]
-        if not (
-            string_leaf.type == token.STRING
-            and "\\\n" in string_leaf.value
-            and not has_triple_quotes(string_leaf.value)
-        ):
+        indices_to_transform = []
+        for string_idx in string_indices:
+            string_leaf = LL[string_idx]
+            if (
+                string_leaf.type == token.STRING
+                and "\\\n" in string_leaf.value
+                and not has_triple_quotes(string_leaf.value)
+            ):
+                indices_to_transform.append(string_idx)
+
+        if not indices_to_transform:
             return TErr(
-                f"String leaf {string_leaf} does not contain any backslash line"
-                " continuation characters."
+                "Found no string leaves that contain backslash line continuation"
+                " characters."
             )
 
         new_line = line.clone()
         new_line.comments = line.comments.copy()
         append_leaves(new_line, line, LL)
 
-        new_string_leaf = new_line.leaves[string_idx]
-        new_string_leaf.value = new_string_leaf.value.replace("\\\n", "")
+        for string_idx in indices_to_transform:
+            new_string_leaf = new_line.leaves[string_idx]
+            new_string_leaf.value = new_string_leaf.value.replace("\\\n", "")
 
         return Ok(new_line)
 
-    def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]:
+    def _merge_string_group(
+        self, line: Line, string_indices: List[int]
+    ) -> TResult[Line]:
         """
-        Merges string group (i.e. set of adjacent strings) where the first
-        string in the group is `line.leaves[string_idx]`.
+        Merges string groups (i.e. set of adjacent strings).
+
+        Each index from `string_indices` designates one string group's first
+        leaf in `line.leaves`.
 
         Returns:
             Ok(new_line), if ALL of the validation checks found in
-            __validate_msg(...) pass.
+            _validate_msg(...) pass.
                 OR
             Err(CannotTransform), otherwise.
         """
@@ -359,10 +516,54 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
 
         is_valid_index = is_valid_index_factory(LL)
 
-        vresult = self._validate_msg(line, string_idx)
-        if isinstance(vresult, Err):
-            return vresult
+        # A dict of {string_idx: tuple[num_of_strings, string_leaf]}.
+        merged_string_idx_dict: Dict[int, Tuple[int, Leaf]] = {}
+        for string_idx in string_indices:
+            vresult = self._validate_msg(line, string_idx)
+            if isinstance(vresult, Err):
+                continue
+            merged_string_idx_dict[string_idx] = self._merge_one_string_group(
+                LL, string_idx, is_valid_index
+            )
+
+        if not merged_string_idx_dict:
+            return TErr("No string group is merged")
+
+        # Build the final line ('new_line') that this method will later return.
+        new_line = line.clone()
+        previous_merged_string_idx = -1
+        previous_merged_num_of_strings = -1
+        for i, leaf in enumerate(LL):
+            if i in merged_string_idx_dict:
+                previous_merged_string_idx = i
+                previous_merged_num_of_strings, string_leaf = merged_string_idx_dict[i]
+                new_line.append(string_leaf)
+
+            if (
+                previous_merged_string_idx
+                <= i
+                < previous_merged_string_idx + previous_merged_num_of_strings
+            ):
+                for comment_leaf in line.comments_after(LL[i]):
+                    new_line.append(comment_leaf, preformatted=True)
+                continue
+
+            append_leaves(new_line, line, [leaf])
+
+        return Ok(new_line)
 
+    def _merge_one_string_group(
+        self, LL: List[Leaf], string_idx: int, is_valid_index: Callable[[int], bool]
+    ) -> Tuple[int, Leaf]:
+        """
+        Merges one string group where the first string in the group is
+        `LL[string_idx]`.
+
+        Returns:
+            A tuple of `(num_of_strings, leaf)` where `num_of_strings` is the
+            number of strings merged and `leaf` is the newly merged string
+            to be replaced in the new line.
+        """
         # If the string group is wrapped inside an Atom node, we must make sure
         # to later replace that Atom with our new (merged) string leaf.
         atom_node = LL[string_idx].parent
@@ -388,6 +589,12 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
                 characters have been escaped.
             """
             assert_is_leaf_string(string)
+            if "f" in string_prefix:
+                string = _toggle_fexpr_quotes(string, QUOTE)
+                # After quotes toggling, quotes in expressions won't be escaped
+                # because quotes can't be reused in f-strings. So we can simply
+                # let the escaping logic below run without knowing f-string
+                # expressions.
 
             RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)"
             naked_string = string[len(string_prefix) + 1 : -1]
@@ -438,7 +645,7 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
             # with 'f'...
             if "f" in prefix and "f" not in next_prefix:
                 # Then we must escape any braces contained in this substring.
-                SS = re.subf(r"(\{|\})", "{1}{1}", SS)
+                SS = re.sub(r"(\{|\})", r"\1\1", SS)
 
             NSS = make_naked(SS, next_prefix)
 
@@ -450,6 +657,9 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
 
             next_str_idx += 1
 
+        # Take a note on the index of the non-STRING leaf.
+        non_string_idx = next_str_idx
+
         S_leaf = Leaf(token.STRING, S)
         if self.normalize_strings:
             S_leaf.value = normalize_string_quotes(S_leaf.value)
@@ -469,29 +679,27 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
         string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, ""))
 
         if atom_node is not None:
-            replace_child(atom_node, string_leaf)
-
-        # Build the final line ('new_line') that this method will later return.
-        new_line = line.clone()
-        for (i, leaf) in enumerate(LL):
-            if i == string_idx:
-                new_line.append(string_leaf)
-
-            if string_idx <= i < string_idx + num_of_strings:
-                for comment_leaf in line.comments_after(LL[i]):
-                    new_line.append(comment_leaf, preformatted=True)
-                continue
-
-            append_leaves(new_line, line, [leaf])
+            # If not all children of the atom node are merged (this can happen
+            # when there is a standalone comment in the middle) ...
+            if non_string_idx - string_idx < len(atom_node.children):
+                # We need to replace the old STRING leaves with the new string leaf.
+                first_child_idx = LL[string_idx].remove()
+                for idx in range(string_idx + 1, non_string_idx):
+                    LL[idx].remove()
+                if first_child_idx is not None:
+                    atom_node.insert_child(first_child_idx, string_leaf)
+            else:
+                # Else replace the atom node with the new string leaf.
+                replace_child(atom_node, string_leaf)
 
         self.add_custom_splits(string_leaf.value, custom_splits)
-        return Ok(new_line)
+        return num_of_strings, string_leaf
 
     @staticmethod
     def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
         """Validate (M)erge (S)tring (G)roup
 
-        Transform-time string validation logic for __merge_string_group(...).
+        Transform-time string validation logic for _merge_string_group(...).
 
         Returns:
             * Ok(None), if ALL validation checks (listed below) pass.
@@ -505,6 +713,11 @@ class StringMerger(CustomSplitMapMixin, StringTransformer):
                 - The set of all string prefixes in the string group is of
                   length greater than one and is not equal to {"", "f"}.
                 - The string group consists of raw strings.
+                - The string group is stringified type annotations. We don't want to
+                  process stringified type annotations since pyright doesn't support
+                  them spanning multiple string values. (NOTE: mypy, pytype, pyre do
+                  support them, so we can change if pyright also gains support in the
+                  future. See https://github.com/microsoft/pyright/issues/4359.)
         """
         # We first check for "inner" stand-alone comments (i.e. stand-alone
         # comments that have a string leaf before them AND after them).
@@ -594,7 +807,15 @@ class StringParenStripper(StringTransformer):
 
         is_valid_index = is_valid_index_factory(LL)
 
-        for (idx, leaf) in enumerate(LL):
+        string_indices = []
+
+        idx = -1
+        while True:
+            idx += 1
+            if idx >= len(LL):
+                break
+            leaf = LL[idx]
+
             # Should be a string...
             if leaf.type != token.STRING:
                 continue
@@ -676,45 +897,76 @@ class StringParenStripper(StringTransformer):
                 }:
                     continue
 
-                return Ok(string_idx)
+                string_indices.append(string_idx)
+                idx = string_idx
+                while idx < len(LL) - 1 and LL[idx + 1].type == token.STRING:
+                    idx += 1
 
+        if string_indices:
+            return Ok(string_indices)
         return TErr("This line has no strings wrapped in parens.")
 
-    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+    def do_transform(
+        self, line: Line, string_indices: List[int]
+    ) -> Iterator[TResult[Line]]:
         LL = line.leaves
 
-        string_parser = StringParser()
-        rpar_idx = string_parser.parse(LL, string_idx)
+        string_and_rpar_indices: List[int] = []
+        for string_idx in string_indices:
+            string_parser = StringParser()
+            rpar_idx = string_parser.parse(LL, string_idx)
+
+            should_transform = True
+            for leaf in (LL[string_idx - 1], LL[rpar_idx]):
+                if line.comments_after(leaf):
+                    # Should not strip parentheses which have comments attached
+                    # to them.
+                    should_transform = False
+                    break
+            if should_transform:
+                string_and_rpar_indices.extend((string_idx, rpar_idx))
 
-        for leaf in (LL[string_idx - 1], LL[rpar_idx]):
-            if line.comments_after(leaf):
-                yield TErr(
-                    "Will not strip parentheses which have comments attached to them."
-                )
-                return
+        if string_and_rpar_indices:
+            yield Ok(self._transform_to_new_line(line, string_and_rpar_indices))
+        else:
+            yield Err(
+                CannotTransform("All string groups have comments attached to them.")
+            )
+
+    def _transform_to_new_line(
+        self, line: Line, string_and_rpar_indices: List[int]
+    ) -> Line:
+        LL = line.leaves
 
         new_line = line.clone()
         new_line.comments = line.comments.copy()
-        try:
-            append_leaves(new_line, line, LL[: string_idx - 1])
-        except BracketMatchError:
-            # HACK: I believe there is currently a bug somewhere in
-            # right_hand_split() that is causing brackets to not be tracked
-            # properly by a shared BracketTracker.
-            append_leaves(new_line, line, LL[: string_idx - 1], preformatted=True)
-
-        string_leaf = Leaf(token.STRING, LL[string_idx].value)
-        LL[string_idx - 1].remove()
-        replace_child(LL[string_idx], string_leaf)
-        new_line.append(string_leaf)
-
-        append_leaves(
-            new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :]
-        )
 
-        LL[rpar_idx].remove()
+        previous_idx = -1
+        # We need to sort the indices, since string_idx and its matching
+        # rpar_idx may not come in order, e.g. in
+        # `("outer" % ("inner".join(items)))`, the "inner" string's
+        # string_idx is smaller than "outer" string's rpar_idx.
+        for idx in sorted(string_and_rpar_indices):
+            leaf = LL[idx]
+            lpar_or_rpar_idx = idx - 1 if leaf.type == token.STRING else idx
+            append_leaves(new_line, line, LL[previous_idx + 1 : lpar_or_rpar_idx])
+            if leaf.type == token.STRING:
+                string_leaf = Leaf(token.STRING, LL[idx].value)
+                LL[lpar_or_rpar_idx].remove()  # Remove lpar.
+                replace_child(LL[idx], string_leaf)
+                new_line.append(string_leaf)
+                # replace comments
+                old_comments = new_line.comments.pop(id(LL[idx]), [])
+                new_line.comments.setdefault(id(string_leaf), []).extend(old_comments)
+            else:
+                LL[lpar_or_rpar_idx].remove()  # This is a rpar.
+
+            previous_idx = idx
 
-        yield Ok(new_line)
+        # Append the leaves after the last idx:
+        append_leaves(new_line, line, LL[idx + 1 :])
+
+        return new_line
 
 
 class BaseStringSplitter(StringTransformer):
@@ -725,21 +977,24 @@ class BaseStringSplitter(StringTransformer):
 
     Requirements:
         * The target string value is responsible for the line going over the
-        line length limit. It follows that after all of black's other line
-        split methods have been exhausted, this line (or one of the resulting
-        lines after all line splits are performed) would still be over the
-        line_length limit unless we split this string.
-            AND
+          line length limit. It follows that after all of black's other line
+          split methods have been exhausted, this line (or one of the resulting
+          lines after all line splits are performed) would still be over the
+          line_length limit unless we split this string.
+          AND
+
         * The target string is NOT a "pointless" string (i.e. a string that has
-        no parent or siblings).
-            AND
+          no parent or siblings).
+          AND
+
         * The target string is not followed by an inline comment that appears
-        to be a pragma.
-            AND
+          to be a pragma.
+          AND
+
         * The target string is not a multiline (i.e. triple-quote) string.
     """
 
-    STRING_OPERATORS = [
+    STRING_OPERATORS: Final = [
         token.EQEQUAL,
         token.GREATER,
         token.GREATEREQUAL,
@@ -767,7 +1022,12 @@ class BaseStringSplitter(StringTransformer):
         if isinstance(match_result, Err):
             return match_result
 
-        string_idx = match_result.ok()
+        string_indices = match_result.ok()
+        assert len(string_indices) == 1, (
+            f"{self.__class__.__name__} should only find one match at a time, found"
+            f" {len(string_indices)}"
+        )
+        string_idx = string_indices[0]
         vresult = self._validate(line, string_idx)
         if isinstance(vresult, Err):
             return vresult
@@ -782,7 +1042,7 @@ class BaseStringSplitter(StringTransformer):
 
         Returns:
             * Ok(None), if ALL of the requirements are met.
-                OR
+              OR
             * Err(CannotTransform), if ANY of the requirements are NOT met.
         """
         LL = line.leaves
@@ -923,20 +1183,140 @@ class BaseStringSplitter(StringTransformer):
             # WMA4 the length of the inline comment.
             offset += len(comment_leaf.value)
 
-        max_string_length = self.line_length - offset
+        max_string_length = count_chars_in_width(str(line), self.line_length - offset)
         return max_string_length
 
+    @staticmethod
+    def _prefer_paren_wrap_match(LL: List[Leaf]) -> Optional[int]:
+        """
+        Returns:
+            string_idx such that @LL[string_idx] is equal to our target (i.e.
+            matched) string, if this line matches the "prefer paren wrap" statement
+            requirements listed in the 'Requirements' section of the StringParenWrapper
+            class's docstring.
+                OR
+            None, otherwise.
+        """
+        # The line must start with a string.
+        if LL[0].type != token.STRING:
+            return None
+
+        matching_nodes = [
+            syms.listmaker,
+            syms.dictsetmaker,
+            syms.testlist_gexp,
+        ]
+        # If the string is an immediate child of a list/set/tuple literal...
+        if (
+            parent_type(LL[0]) in matching_nodes
+            or parent_type(LL[0].parent) in matching_nodes
+        ):
+            # And the string is surrounded by commas (or is the first/last child)...
+            prev_sibling = LL[0].prev_sibling
+            next_sibling = LL[0].next_sibling
+            if (
+                not prev_sibling
+                and not next_sibling
+                and parent_type(LL[0]) == syms.atom
+            ):
+                # If it's an atom string, we need to check the parent atom's siblings.
+                parent = LL[0].parent
+                assert parent is not None  # For type checkers.
+                prev_sibling = parent.prev_sibling
+                next_sibling = parent.next_sibling
+            if (not prev_sibling or prev_sibling.type == token.COMMA) and (
+                not next_sibling or next_sibling.type == token.COMMA
+            ):
+                return 0
 
-class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
+        return None
+
+
+def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]:
+    """
+    Yields spans corresponding to expressions in a given f-string.
+    Spans are half-open ranges (left inclusive, right exclusive).
+    Assumes the input string is a valid f-string, but will not crash if the input
+    string is invalid.
+    """
+    stack: List[int] = []  # our curly paren stack
+    i = 0
+    while i < len(s):
+        if s[i] == "{":
+            # if we're in a string part of the f-string, ignore escaped curly braces
+            if not stack and i + 1 < len(s) and s[i + 1] == "{":
+                i += 2
+                continue
+            stack.append(i)
+            i += 1
+            continue
+
+        if s[i] == "}":
+            if not stack:
+                i += 1
+                continue
+            j = stack.pop()
+            # we've made it back out of the expression! yield the span
+            if not stack:
+                yield (j, i + 1)
+            i += 1
+            continue
+
+        # if we're in an expression part of the f-string, fast forward through strings
+        # note that backslashes are not legal in the expression portion of f-strings
+        if stack:
+            delim = None
+            if s[i : i + 3] in ("'''", '"""'):
+                delim = s[i : i + 3]
+            elif s[i] in ("'", '"'):
+                delim = s[i]
+            if delim:
+                i += len(delim)
+                while i < len(s) and s[i : i + len(delim)] != delim:
+                    i += 1
+                i += len(delim)
+                continue
+        i += 1
+
+
+def fstring_contains_expr(s: str) -> bool:
+    return any(iter_fexpr_spans(s))
+
+
+def _toggle_fexpr_quotes(fstring: str, old_quote: str) -> str:
+    """
+    Toggles quotes used in f-string expressions that are `old_quote`.
+
+    f-string expressions can't contain backslashes, so we need to toggle the
+    quotes if the f-string itself will end up using the same quote. We can
+    simply toggle without escaping because, quotes can't be reused in f-string
+    expressions. They will fail to parse.
+
+    NOTE: If PEP 701 is accepted, above statement will no longer be true.
+    Though if quotes can be reused, we can simply reuse them without updates or
+    escaping, once Black figures out how to parse the new grammar.
+    """
+    new_quote = "'" if old_quote == '"' else '"'
+    parts = []
+    previous_index = 0
+    for start, end in iter_fexpr_spans(fstring):
+        parts.append(fstring[previous_index:start])
+        parts.append(fstring[start:end].replace(old_quote, new_quote))
+        previous_index = end
+    parts.append(fstring[previous_index:])
+    return "".join(parts)
+
+
+class StringSplitter(BaseStringSplitter, CustomSplitMapMixin):
     """
     StringTransformer that splits "atom" strings (i.e. strings which exist on
     lines by themselves).
 
     Requirements:
         * The line consists ONLY of a single string (possibly prefixed by a
-        string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
-        a trailing comma.
-            AND
+          string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
+          a trailing comma.
+          AND
         * All of the requirements listed in BaseStringSplitter's docstring.
 
     Transformations:
@@ -965,22 +1345,14 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
         CustomSplit objects and add them to the custom split map.
     """
 
-    MIN_SUBSTR_SIZE = 6
-    # Matches an "f-expression" (e.g. {var}) that might be found in an f-string.
-    RE_FEXPR = r"""
-    (?<!\{) (?:\{\{)* \{ (?!\{)
-        (?:
-            [^\{\}]
-            | \{\{
-            | \}\}
-            | (?R)
-        )+
-    \}
-    """
+    MIN_SUBSTR_SIZE: Final = 6
 
     def do_splitter_match(self, line: Line) -> TMatchResult:
         LL = line.leaves
 
+        if self._prefer_paren_wrap_match(LL) is not None:
+            return TErr("Line needs to be wrapped in parens first.")
+
         is_valid_index = is_valid_index_factory(LL)
 
         idx = 0
@@ -1027,10 +1399,17 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
         if is_valid_index(idx):
             return TErr("This line does not end with a string.")
 
-        return Ok(string_idx)
+        return Ok([string_idx])
 
-    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+    def do_transform(
+        self, line: Line, string_indices: List[int]
+    ) -> Iterator[TResult[Line]]:
         LL = line.leaves
+        assert len(string_indices) == 1, (
+            f"{self.__class__.__name__} should only find one match at a time, found"
+            f" {len(string_indices)}"
+        )
+        string_idx = string_indices[0]
 
         QUOTE = LL[string_idx].value[-1]
 
@@ -1043,15 +1422,15 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
         # contain any f-expressions, but ONLY if the original f-string
         # contains at least one f-expression. Otherwise, we will alter the AST
         # of the program.
-        drop_pointless_f_prefix = ("f" in prefix) and re.search(
-            self.RE_FEXPR, LL[string_idx].value, re.VERBOSE
+        drop_pointless_f_prefix = ("f" in prefix) and fstring_contains_expr(
+            LL[string_idx].value
         )
 
         first_string_line = True
 
         string_op_leaves = self._get_string_operator_leaves(LL)
         string_op_leaves_length = (
-            sum([len(str(prefix_leaf)) for prefix_leaf in string_op_leaves]) + 1
+            sum(len(str(prefix_leaf)) for prefix_leaf in string_op_leaves) + 1
             if string_op_leaves
             else 0
         )
@@ -1073,11 +1452,13 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
             is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
         )
 
-        def max_last_string() -> int:
+        def max_last_string_column() -> int:
             """
             Returns:
-                The max allowed length of the string value used for the last
-                line we will construct.
+                The max allowed width of the string value used for the last
+                line we will construct.  Note that this value means the width
+                rather than the number of characters (e.g., many East Asian
+                characters expand to two columns).
             """
             result = self.line_length
             result -= line.depth * 4
@@ -1085,14 +1466,14 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
             result -= string_op_leaves_length
             return result
 
-        # --- Calculate Max Break Index (for string value)
+        # --- Calculate Max Break Width (for string value)
         # We start with the line length limit
-        max_break_idx = self.line_length
+        max_break_width = self.line_length
         # The last index of a string of length N is N-1.
-        max_break_idx -= 1
+        max_break_width -= 1
         # Leading whitespace is not present in the string value (e.g. Leaf.value).
-        max_break_idx -= line.depth * 4
-        if max_break_idx < 0:
+        max_break_width -= line.depth * 4
+        if max_break_width < 0:
             yield TErr(
                 f"Unable to split {LL[string_idx].value} at such high of a line depth:"
                 f" {line.depth}"
@@ -1105,7 +1486,7 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
         # line limit.
         use_custom_breakpoints = bool(
             custom_splits
-            and all(csplit.break_idx <= max_break_idx for csplit in custom_splits)
+            and all(csplit.break_idx <= max_break_width for csplit in custom_splits)
         )
 
         # Temporary storage for the remaining chunk of the string line that
@@ -1121,7 +1502,7 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
             if use_custom_breakpoints:
                 return len(custom_splits) > 1
             else:
-                return len(rest_value) > max_last_string()
+                return str_width(rest_value) > max_last_string_column()
 
         string_line_results: List[Ok[Line]] = []
         while more_splits_should_be_made():
@@ -1131,7 +1512,10 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
                 break_idx = csplit.break_idx
             else:
                 # Algorithmic Split (automatic)
-                max_bidx = max_break_idx - string_op_leaves_length
+                max_bidx = (
+                    count_chars_in_width(rest_value, max_break_width)
+                    - string_op_leaves_length
+                )
                 maybe_break_idx = self._get_break_idx(rest_value, max_bidx)
                 if maybe_break_idx is None:
                     # If we are unable to algorithmically determine a good split
@@ -1168,9 +1552,14 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
             # prefix, and the current custom split did NOT originally use a
             # prefix...
             if (
-                next_value != self._normalize_f_string(next_value, prefix)
-                and use_custom_breakpoints
+                use_custom_breakpoints
                 and not csplit.has_prefix
+                and (
+                    # `next_value == prefix + QUOTE` happens when the custom
+                    # split is an empty string.
+                    next_value == prefix + QUOTE
+                    or next_value != self._normalize_f_string(next_value, prefix)
+                )
             ):
                 # Then `csplit.break_idx` will be off by one after removing
                 # the 'f' prefix.
@@ -1223,7 +1612,7 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
 
             # Try to fit them all on the same line with the last substring...
             if (
-                len(temp_value) <= max_last_string()
+                str_width(temp_value) <= max_last_string_column()
                 or LL[string_idx + 1].type == token.COMMA
             ):
                 last_line.append(rest_leaf)
@@ -1284,9 +1673,7 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
         """
         if "f" not in get_string_prefix(string).lower():
             return
-
-        for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE):
-            yield match.span()
+        yield from iter_fexpr_spans(string)
 
     def _get_illegal_split_indices(self, string: str) -> Set[Index]:
         illegal_indices: Set[Index] = set()
@@ -1345,6 +1732,7 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
                 section of this classes' docstring would be be met by returning @i.
             """
             is_space = string[i] == " "
+            is_split_safe = is_valid_index(i - 1) and string[i - 1] in SPLIT_SAFE_CHARS
 
             is_not_escaped = True
             j = i - 1
@@ -1357,7 +1745,7 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
                 and len(string[:i]) >= self.MIN_SUBSTR_SIZE
             )
             return (
-                is_space
+                (is_space or is_split_safe)
                 and is_not_escaped
                 and is_big_enough
                 and not breaks_unsplittable_expression(i)
@@ -1402,7 +1790,7 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
         """
         assert_is_leaf_string(string)
 
-        if "f" in prefix and not re.search(self.RE_FEXPR, string, re.VERBOSE):
+        if "f" in prefix and not fstring_contains_expr(string):
             new_prefix = prefix.replace("f", "")
 
             temp = string[len(prefix) :]
@@ -1426,29 +1814,35 @@ class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
         return string_op_leaves
 
 
-class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
+class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
     """
-    StringTransformer that splits non-"atom" strings (i.e. strings that do not
-    exist on lines by themselves).
+    StringTransformer that wraps strings in parens and then splits at the LPAR.
 
     Requirements:
         All of the requirements listed in BaseStringSplitter's docstring in
         addition to the requirements listed below:
 
         * The line is a return/yield statement, which returns/yields a string.
-            OR
+          OR
         * The line is part of a ternary expression (e.g. `x = y if cond else
-        z`) such that the line starts with `else <string>`, where <string> is
-        some string.
-            OR
+          z`) such that the line starts with `else <string>`, where <string> is
+          some string.
+          OR
         * The line is an assert statement, which ends with a string.
-            OR
+          OR
         * The line is an assignment statement (e.g. `x = <string>` or `x +=
-        <string>`) such that the variable is being assigned the value of some
-        string.
-            OR
+          <string>`) such that the variable is being assigned the value of some
+          string.
+          OR
         * The line is a dictionary key assignment where some valid key is being
-        assigned the value of some string.
+          assigned the value of some string.
+          OR
+        * The line is an lambda expression and the value is a string.
+          OR
+        * The line starts with an "atom" string that prefers to be wrapped in
+          parens. It's preferred to be wrapped when it's is an immediate child of
+          a list/set/tuple literal, AND the string is surrounded by commas (or is
+          the first/last child).
 
     Transformations:
         The chosen string is wrapped in parentheses and then split at the LPAR.
@@ -1473,6 +1867,9 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
         changed such that it no longer needs to be given its own line,
         StringParenWrapper relies on StringParenStripper to clean up the
         parentheses it created.
+
+        For "atom" strings that prefers to be wrapped in parens, it requires
+        StringSplitter to hold the split until the string is wrapped in parens.
     """
 
     def do_splitter_match(self, line: Line) -> TMatchResult:
@@ -1488,16 +1885,19 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
             or self._else_match(LL)
             or self._assert_match(LL)
             or self._assign_match(LL)
-            or self._dict_match(LL)
+            or self._dict_or_lambda_match(LL)
+            or self._prefer_paren_wrap_match(LL)
         )
 
         if string_idx is not None:
             string_value = line.leaves[string_idx].value
-            # If the string has no spaces...
-            if " " not in string_value:
+            # If the string has neither spaces nor East Asian stops...
+            if not any(
+                char == " " or char in SPLIT_SAFE_CHARS for char in string_value
+            ):
                 # And will still violate the line length limit when split...
-                max_string_length = self.line_length - ((line.depth + 1) * 4)
-                if len(string_value) > max_string_length:
+                max_string_width = self.line_length - ((line.depth + 1) * 4)
+                if str_width(string_value) > max_string_width:
                     # And has no associated custom splits...
                     if not self.has_custom_splits(string_value):
                         # Then we should NOT put this string on its own line.
@@ -1506,7 +1906,7 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
                             " resultant line would still be over the specified line"
                             " length and can't be split further by StringSplitter."
                         )
-            return Ok(string_idx)
+            return Ok([string_idx])
 
         return TErr("This line does not contain any non-atomic strings.")
 
@@ -1578,7 +1978,7 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
         if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert":
             is_valid_index = is_valid_index_factory(LL)
 
-            for (i, leaf) in enumerate(LL):
+            for i, leaf in enumerate(LL):
                 # We MUST find a comma...
                 if leaf.type == token.COMMA:
                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
@@ -1616,7 +2016,7 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
         ):
             is_valid_index = is_valid_index_factory(LL)
 
-            for (i, leaf) in enumerate(LL):
+            for i, leaf in enumerate(LL):
                 # We MUST find either an '=' or '+=' symbol...
                 if leaf.type in [token.EQUAL, token.PLUSEQUAL]:
                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
@@ -1645,23 +2045,24 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
         return None
 
     @staticmethod
-    def _dict_match(LL: List[Leaf]) -> Optional[int]:
+    def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]:
         """
         Returns:
             string_idx such that @LL[string_idx] is equal to our target (i.e.
             matched) string, if this line matches the dictionary key assignment
-            statement requirements listed in the 'Requirements' section of this
-            classes' docstring.
+            statement or lambda expression requirements listed in the
+            'Requirements' section of this classes' docstring.
                 OR
             None, otherwise.
         """
-        # If this line is apart of a dictionary key assignment...
-        if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]:
+        # If this line is a part of a dictionary key assignment or lambda expression...
+        parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)]
+        if syms.dictsetmaker in parent_types or syms.lambdef in parent_types:
             is_valid_index = is_valid_index_factory(LL)
 
-            for (i, leaf) in enumerate(LL):
-                # We MUST find a colon...
-                if leaf.type == token.COLON:
+            for i, leaf in enumerate(LL):
+                # We MUST find a colon, it can either be dict's or lambda's colon...
+                if leaf.type == token.COLON and i < len(LL) - 1:
                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
 
                     # That colon MUST be followed by a string...
@@ -1682,8 +2083,15 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
 
         return None
 
-    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+    def do_transform(
+        self, line: Line, string_indices: List[int]
+    ) -> Iterator[TResult[Line]]:
         LL = line.leaves
+        assert len(string_indices) == 1, (
+            f"{self.__class__.__name__} should only find one match at a time, found"
+            f" {len(string_indices)}"
+        )
+        string_idx = string_indices[0]
 
         is_valid_index = is_valid_index_factory(LL)
         insert_str_child = insert_str_child_factory(LL[string_idx])
@@ -1755,6 +2163,25 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
                     f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
                 )
                 old_rpar_leaf = right_leaves.pop()
+            elif right_leaves and right_leaves[-1].type == token.RPAR:
+                # Special case for lambda expressions as dict's value, e.g.:
+                #     my_dict = {
+                #        "key": lambda x: f"formatted: {x},
+                #     }
+                # After wrapping the dict's value with parentheses, the string is
+                # followed by a RPAR but its opening bracket is lambda's, not
+                # the string's:
+                #        "key": (lambda x: f"formatted: {x}),
+                opening_bracket = right_leaves[-1].opening_bracket
+                if opening_bracket is not None and opening_bracket in left_leaves:
+                    index = left_leaves.index(opening_bracket)
+                    if (
+                        index > 0
+                        and index < len(left_leaves) - 1
+                        and left_leaves[index - 1].type == token.COLON
+                        and left_leaves[index + 1].value == "lambda"
+                    ):
+                        right_leaves.pop()
 
             append_leaves(string_line, line, right_leaves)
 
@@ -1811,20 +2238,20 @@ class StringParser:
         ```
     """
 
-    DEFAULT_TOKEN = -1
+    DEFAULT_TOKEN: Final = 20210605
 
     # String Parser States
-    START = 1
-    DOT = 2
-    NAME = 3
-    PERCENT = 4
-    SINGLE_FMT_ARG = 5
-    LPAR = 6
-    RPAR = 7
-    DONE = 8
+    START: Final = 1
+    DOT: Final = 2
+    NAME: Final = 3
+    PERCENT: Final = 4
+    SINGLE_FMT_ARG: Final = 5
+    LPAR: Final = 6
+    RPAR: Final = 7
+    DONE: Final = 8
 
     # Lookup Table for Next State
-    _goto: Dict[Tuple[ParserState, NodeType], ParserState] = {
+    _goto: Final[Dict[Tuple[ParserState, NodeType], ParserState]] = {
         # A string trailer may start with '.' OR '%'.
         (START, token.DOT): DOT,
         (START, token.PERCENT): PERCENT,
@@ -1861,7 +2288,7 @@ class StringParser:
         Returns:
             The index directly after the last leaf which is apart of the string
             trailer, if a "trailer" exists.
-                OR
+            OR
             @string_idx + 1, if no string "trailer" exists.
         """
         assert leaves[string_idx].type == token.STRING
@@ -1875,11 +2302,11 @@ class StringParser:
         """
         Pre-conditions:
             * On the first call to this function, @leaf MUST be the leaf that
-            was directly after the string leaf in question (e.g. if our target
-            string is `line.leaves[i]` then the first call to this method must
-            be `line.leaves[i + 1]`).
+              was directly after the string leaf in question (e.g. if our target
+              string is `line.leaves[i]` then the first call to this method must
+              be `line.leaves[i + 1]`).
             * On the next call to this function, the leaf parameter passed in
-            MUST be the leaf directly following @leaf.
+              MUST be the leaf directly following @leaf.
 
         Returns:
             True iff @leaf is apart of the string's trailer.
diff --git a/.vim/bundle/black/src/black_primer/cli.py b/.vim/bundle/black/src/black_primer/cli.py
deleted file mode 100644 (file)
index 8360fc3..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-# coding=utf8
-
-import asyncio
-import logging
-import sys
-from datetime import datetime
-from pathlib import Path
-from shutil import rmtree, which
-from tempfile import gettempdir
-from typing import Any, Union, Optional
-
-import click
-
-from black_primer import lib
-
-# If our environment has uvloop installed lets use it
-try:
-    import uvloop
-
-    uvloop.install()
-except ImportError:
-    pass
-
-
-DEFAULT_CONFIG = Path(__file__).parent / "primer.json"
-_timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
-DEFAULT_WORKDIR = Path(gettempdir()) / f"primer.{_timestamp}"
-LOG = logging.getLogger(__name__)
-
-
-def _handle_debug(
-    ctx: Optional[click.core.Context],
-    param: Optional[Union[click.core.Option, click.core.Parameter]],
-    debug: Union[bool, int, str],
-) -> Union[bool, int, str]:
-    """Turn on debugging if asked otherwise INFO default"""
-    log_level = logging.DEBUG if debug else logging.INFO
-    logging.basicConfig(
-        format="[%(asctime)s] %(levelname)s: %(message)s (%(filename)s:%(lineno)d)",
-        level=log_level,
-    )
-    return debug
-
-
-async def async_main(
-    config: str,
-    debug: bool,
-    keep: bool,
-    long_checkouts: bool,
-    no_diff: bool,
-    rebase: bool,
-    workdir: str,
-    workers: int,
-) -> int:
-    work_path = Path(workdir)
-    if not work_path.exists():
-        LOG.debug(f"Creating {work_path}")
-        work_path.mkdir()
-
-    if not which("black"):
-        LOG.error("Can not find 'black' executable in PATH. No point in running")
-        return -1
-
-    try:
-        ret_val = await lib.process_queue(
-            config,
-            work_path,
-            workers,
-            keep,
-            long_checkouts,
-            rebase,
-            no_diff,
-        )
-        return int(ret_val)
-    finally:
-        if not keep and work_path.exists():
-            LOG.debug(f"Removing {work_path}")
-            rmtree(work_path, onerror=lib.handle_PermissionError)
-
-    return -2
-
-
-@click.command(context_settings={"help_option_names": ["-h", "--help"]})
-@click.option(
-    "-c",
-    "--config",
-    default=str(DEFAULT_CONFIG),
-    type=click.Path(exists=True),
-    show_default=True,
-    help="JSON config file path",
-)
-@click.option(
-    "--debug",
-    is_flag=True,
-    callback=_handle_debug,
-    show_default=True,
-    help="Turn on debug logging",
-)
-@click.option(
-    "-k",
-    "--keep",
-    is_flag=True,
-    show_default=True,
-    help="Keep workdir + repos post run",
-)
-@click.option(
-    "-L",
-    "--long-checkouts",
-    is_flag=True,
-    show_default=True,
-    help="Pull big projects to test",
-)
-@click.option(
-    "--no-diff",
-    is_flag=True,
-    show_default=True,
-    help="Disable showing source file changes in black output",
-)
-@click.option(
-    "-R",
-    "--rebase",
-    is_flag=True,
-    show_default=True,
-    help="Rebase project if already checked out",
-)
-@click.option(
-    "-w",
-    "--workdir",
-    default=str(DEFAULT_WORKDIR),
-    type=click.Path(exists=False),
-    show_default=True,
-    help="Directory path for repo checkouts",
-)
-@click.option(
-    "-W",
-    "--workers",
-    default=2,
-    type=int,
-    show_default=True,
-    help="Number of parallel worker coroutines",
-)
-@click.pass_context
-def main(ctx: click.core.Context, **kwargs: Any) -> None:
-    """primer - prime projects for blackening... 🏴"""
-    LOG.debug(f"Starting {sys.argv[0]}")
-    # TODO: Change to asyncio.run when Black >= 3.7 only
-    loop = asyncio.get_event_loop()
-    try:
-        ctx.exit(loop.run_until_complete(async_main(**kwargs)))
-    finally:
-        loop.close()
-
-
-if __name__ == "__main__":  # pragma: nocover
-    main()
diff --git a/.vim/bundle/black/src/black_primer/lib.py b/.vim/bundle/black/src/black_primer/lib.py
deleted file mode 100644 (file)
index 7494ae6..0000000
+++ /dev/null
@@ -1,418 +0,0 @@
-import asyncio
-import errno
-import json
-import logging
-import os
-import stat
-import sys
-from functools import partial
-from pathlib import Path
-from platform import system
-from shutil import rmtree, which
-from subprocess import CalledProcessError
-from sys import version_info
-from tempfile import TemporaryDirectory
-from typing import (
-    Any,
-    Callable,
-    Dict,
-    List,
-    NamedTuple,
-    Optional,
-    Sequence,
-    Tuple,
-    Union,
-)
-from urllib.parse import urlparse
-
-import click
-
-
-TEN_MINUTES_SECONDS = 600
-WINDOWS = system() == "Windows"
-BLACK_BINARY = "black.exe" if WINDOWS else "black"
-GIT_BINARY = "git.exe" if WINDOWS else "git"
-LOG = logging.getLogger(__name__)
-
-
-# Windows needs a ProactorEventLoop if you want to exec subprocesses
-# Starting with 3.8 this is the default - can remove when Black >= 3.8
-# mypy only respects sys.platform if directly in the evaluation
-# https://mypy.readthedocs.io/en/latest/common_issues.html#python-version-and-system-platform-checks  # noqa: B950
-if sys.platform == "win32":
-    asyncio.set_event_loop(asyncio.ProactorEventLoop())
-
-
-class Results(NamedTuple):
-    stats: Dict[str, int] = {}
-    failed_projects: Dict[str, CalledProcessError] = {}
-
-
-async def _gen_check_output(
-    cmd: Sequence[str],
-    timeout: float = TEN_MINUTES_SECONDS,
-    env: Optional[Dict[str, str]] = None,
-    cwd: Optional[Path] = None,
-    stdin: Optional[bytes] = None,
-) -> Tuple[bytes, bytes]:
-    process = await asyncio.create_subprocess_exec(
-        *cmd,
-        stdin=asyncio.subprocess.PIPE,
-        stdout=asyncio.subprocess.PIPE,
-        stderr=asyncio.subprocess.STDOUT,
-        env=env,
-        cwd=cwd,
-    )
-    try:
-        (stdout, stderr) = await asyncio.wait_for(process.communicate(stdin), timeout)
-    except asyncio.TimeoutError:
-        process.kill()
-        await process.wait()
-        raise
-
-    # A non-optional timeout was supplied to asyncio.wait_for, guaranteeing
-    # a timeout or completed process.  A terminated Python process will have a
-    # non-empty returncode value.
-    assert process.returncode is not None
-
-    if process.returncode != 0:
-        cmd_str = " ".join(cmd)
-        raise CalledProcessError(
-            process.returncode, cmd_str, output=stdout, stderr=stderr
-        )
-
-    return (stdout, stderr)
-
-
-def analyze_results(project_count: int, results: Results) -> int:
-    failed_pct = round(((results.stats["failed"] / project_count) * 100), 2)
-    success_pct = round(((results.stats["success"] / project_count) * 100), 2)
-
-    click.secho("-- primer results 📊 --\n", bold=True)
-    click.secho(
-        f"{results.stats['success']} / {project_count} succeeded ({success_pct}%) ✅",
-        bold=True,
-        fg="green",
-    )
-    click.secho(
-        f"{results.stats['failed']} / {project_count} FAILED ({failed_pct}%) 💩",
-        bold=bool(results.stats["failed"]),
-        fg="red",
-    )
-    s = "" if results.stats["disabled"] == 1 else "s"
-    click.echo(f" - {results.stats['disabled']} project{s} disabled by config")
-    s = "" if results.stats["wrong_py_ver"] == 1 else "s"
-    click.echo(
-        f" - {results.stats['wrong_py_ver']} project{s} skipped due to Python version"
-    )
-    click.echo(
-        f" - {results.stats['skipped_long_checkout']} skipped due to long checkout"
-    )
-
-    if results.failed_projects:
-        click.secho("\nFailed projects:\n", bold=True)
-
-    for project_name, project_cpe in results.failed_projects.items():
-        print(f"## {project_name}:")
-        print(f" - Returned {project_cpe.returncode}")
-        if project_cpe.stderr:
-            print(f" - stderr:\n{project_cpe.stderr.decode('utf8')}")
-        if project_cpe.stdout:
-            print(f" - stdout:\n{project_cpe.stdout.decode('utf8')}")
-        print("")
-
-    return results.stats["failed"]
-
-
-def _flatten_cli_args(cli_args: List[Union[Sequence[str], str]]) -> List[str]:
-    """Allow a user to put long arguments into a list of strs
-    to make the JSON human readable"""
-    flat_args = []
-    for arg in cli_args:
-        if isinstance(arg, str):
-            flat_args.append(arg)
-            continue
-
-        args_as_str = "".join(arg)
-        flat_args.append(args_as_str)
-
-    return flat_args
-
-
-async def black_run(
-    project_name: str,
-    repo_path: Optional[Path],
-    project_config: Dict[str, Any],
-    results: Results,
-    no_diff: bool = False,
-) -> None:
-    """Run Black and record failures"""
-    if not repo_path:
-        results.stats["failed"] += 1
-        results.failed_projects[project_name] = CalledProcessError(
-            69, [], f"{project_name} has no repo_path: {repo_path}".encode(), b""
-        )
-        return
-
-    stdin_test = project_name.upper() == "STDIN"
-    cmd = [str(which(BLACK_BINARY))]
-    if "cli_arguments" in project_config and project_config["cli_arguments"]:
-        cmd.extend(_flatten_cli_args(project_config["cli_arguments"]))
-    cmd.append("--check")
-    if not no_diff:
-        cmd.append("--diff")
-
-    # Workout if we should read in a python file or search from cwd
-    stdin = None
-    if stdin_test:
-        cmd.append("-")
-        stdin = repo_path.read_bytes()
-    elif "base_path" in project_config:
-        cmd.append(project_config["base_path"])
-    else:
-        cmd.append(".")
-
-    timeout = (
-        project_config["timeout_seconds"]
-        if "timeout_seconds" in project_config
-        else TEN_MINUTES_SECONDS
-    )
-    with TemporaryDirectory() as tmp_path:
-        # Prevent reading top-level user configs by manipulating environment variables
-        env = {
-            **os.environ,
-            "XDG_CONFIG_HOME": tmp_path,  # Unix-like
-            "USERPROFILE": tmp_path,  # Windows (changes `Path.home()` output)
-        }
-
-        cwd_path = repo_path.parent if stdin_test else repo_path
-        try:
-            LOG.debug(f"Running black for {project_name}: {' '.join(cmd)}")
-            _stdout, _stderr = await _gen_check_output(
-                cmd, cwd=cwd_path, env=env, stdin=stdin, timeout=timeout
-            )
-        except asyncio.TimeoutError:
-            results.stats["failed"] += 1
-            LOG.error(f"Running black for {repo_path} timed out ({cmd})")
-        except CalledProcessError as cpe:
-            # TODO: Tune for smarter for higher signal
-            # If any other return value than 1 we raise - can disable project in config
-            if cpe.returncode == 1:
-                if not project_config["expect_formatting_changes"]:
-                    results.stats["failed"] += 1
-                    results.failed_projects[repo_path.name] = cpe
-                else:
-                    results.stats["success"] += 1
-                return
-            elif cpe.returncode > 1:
-                results.stats["failed"] += 1
-                results.failed_projects[repo_path.name] = cpe
-                return
-
-            LOG.error(f"Unknown error with {repo_path}")
-            raise
-
-    # If we get here and expect formatting changes something is up
-    if project_config["expect_formatting_changes"]:
-        results.stats["failed"] += 1
-        results.failed_projects[repo_path.name] = CalledProcessError(
-            0, cmd, b"Expected formatting changes but didn't get any!", b""
-        )
-        return
-
-    results.stats["success"] += 1
-
-
-async def git_checkout_or_rebase(
-    work_path: Path,
-    project_config: Dict[str, Any],
-    rebase: bool = False,
-    *,
-    depth: int = 1,
-) -> Optional[Path]:
-    """git Clone project or rebase"""
-    git_bin = str(which(GIT_BINARY))
-    if not git_bin:
-        LOG.error("No git binary found")
-        return None
-
-    repo_url_parts = urlparse(project_config["git_clone_url"])
-    path_parts = repo_url_parts.path[1:].split("/", maxsplit=1)
-
-    repo_path: Path = work_path / path_parts[1].replace(".git", "")
-    cmd = [git_bin, "clone", "--depth", str(depth), project_config["git_clone_url"]]
-    cwd = work_path
-    if repo_path.exists() and rebase:
-        cmd = [git_bin, "pull", "--rebase"]
-        cwd = repo_path
-    elif repo_path.exists():
-        return repo_path
-
-    try:
-        _stdout, _stderr = await _gen_check_output(cmd, cwd=cwd)
-    except (asyncio.TimeoutError, CalledProcessError) as e:
-        LOG.error(f"Unable to git clone / pull {project_config['git_clone_url']}: {e}")
-        return None
-
-    return repo_path
-
-
-def handle_PermissionError(
-    func: Callable, path: Path, exc: Tuple[Any, Any, Any]
-) -> None:
-    """
-    Handle PermissionError during shutil.rmtree.
-
-    This checks if the erroring function is either 'os.rmdir' or 'os.unlink', and that
-    the error was EACCES (i.e. Permission denied). If true, the path is set writable,
-    readable, and executable by everyone. Finally, it tries the error causing delete
-    operation again.
-
-    If the check is false, then the original error will be reraised as this function
-    can't handle it.
-    """
-    excvalue = exc[1]
-    LOG.debug(f"Handling {excvalue} from {func.__name__}... ")
-    if func in (os.rmdir, os.unlink) and excvalue.errno == errno.EACCES:
-        LOG.debug(f"Setting {path} writable, readable, and executable by everyone... ")
-        os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)  # chmod 0777
-        func(path)  # Try the error causing delete operation again
-    else:
-        raise
-
-
-async def load_projects_queue(
-    config_path: Path,
-) -> Tuple[Dict[str, Any], asyncio.Queue]:
-    """Load project config and fill queue with all the project names"""
-    with config_path.open("r") as cfp:
-        config = json.load(cfp)
-
-    # TODO: Offer more options here
-    # e.g. Run on X random packages or specific sub list etc.
-    project_names = sorted(config["projects"].keys())
-    queue: asyncio.Queue = asyncio.Queue(maxsize=len(project_names))
-    for project in project_names:
-        await queue.put(project)
-
-    return config, queue
-
-
-async def project_runner(
-    idx: int,
-    config: Dict[str, Any],
-    queue: asyncio.Queue,
-    work_path: Path,
-    results: Results,
-    long_checkouts: bool = False,
-    rebase: bool = False,
-    keep: bool = False,
-    no_diff: bool = False,
-) -> None:
-    """Check out project and run Black on it + record result"""
-    loop = asyncio.get_event_loop()
-    py_version = f"{version_info[0]}.{version_info[1]}"
-    while True:
-        try:
-            project_name = queue.get_nowait()
-        except asyncio.QueueEmpty:
-            LOG.debug(f"project_runner {idx} exiting")
-            return
-        LOG.debug(f"worker {idx} working on {project_name}")
-
-        project_config = config["projects"][project_name]
-
-        # Check if disabled by config
-        if "disabled" in project_config and project_config["disabled"]:
-            results.stats["disabled"] += 1
-            LOG.info(f"Skipping {project_name} as it's disabled via config")
-            continue
-
-        # Check if we should run on this version of Python
-        if (
-            "all" not in project_config["py_versions"]
-            and py_version not in project_config["py_versions"]
-        ):
-            results.stats["wrong_py_ver"] += 1
-            LOG.debug(f"Skipping {project_name} as it's not enabled for {py_version}")
-            continue
-
-        # Check if we're doing big projects / long checkouts
-        if not long_checkouts and project_config["long_checkout"]:
-            results.stats["skipped_long_checkout"] += 1
-            LOG.debug(f"Skipping {project_name} as it's configured as a long checkout")
-            continue
-
-        repo_path: Optional[Path] = Path(__file__)
-        stdin_project = project_name.upper() == "STDIN"
-        if not stdin_project:
-            repo_path = await git_checkout_or_rebase(work_path, project_config, rebase)
-            if not repo_path:
-                continue
-        await black_run(project_name, repo_path, project_config, results, no_diff)
-
-        if not keep and not stdin_project:
-            LOG.debug(f"Removing {repo_path}")
-            rmtree_partial = partial(
-                rmtree, path=repo_path, onerror=handle_PermissionError
-            )
-            await loop.run_in_executor(None, rmtree_partial)
-
-        LOG.info(f"Finished {project_name}")
-
-
-async def process_queue(
-    config_file: str,
-    work_path: Path,
-    workers: int,
-    keep: bool = False,
-    long_checkouts: bool = False,
-    rebase: bool = False,
-    no_diff: bool = False,
-) -> int:
-    """
-    Process the queue with X workers and evaluate results
-    - Success is guaged via the config "expect_formatting_changes"
-
-    Integer return equals the number of failed projects
-    """
-    results = Results()
-    results.stats["disabled"] = 0
-    results.stats["failed"] = 0
-    results.stats["skipped_long_checkout"] = 0
-    results.stats["success"] = 0
-    results.stats["wrong_py_ver"] = 0
-
-    config, queue = await load_projects_queue(Path(config_file))
-    project_count = queue.qsize()
-    s = "" if project_count == 1 else "s"
-    LOG.info(f"{project_count} project{s} to run Black over")
-    if project_count < 1:
-        return -1
-
-    s = "" if workers == 1 else "s"
-    LOG.debug(f"Using {workers} parallel worker{s} to run Black")
-    # Wait until we finish running all the projects before analyzing
-    await asyncio.gather(
-        *[
-            project_runner(
-                i,
-                config,
-                queue,
-                work_path,
-                results,
-                long_checkouts,
-                rebase,
-                keep,
-                no_diff,
-            )
-            for i in range(workers)
-        ]
-    )
-
-    LOG.info("Analyzing results")
-    return analyze_results(project_count, results)
-
-
-if __name__ == "__main__":  # pragma: nocover
-    raise NotImplementedError("lib is a library, funnily enough.")
diff --git a/.vim/bundle/black/src/black_primer/primer.json b/.vim/bundle/black/src/black_primer/primer.json
deleted file mode 100644 (file)
index 0d1018f..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-{
-  "configuration_format_version": 20210815,
-  "projects": {
-    "STDIN": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": false,
-      "git_clone_url": "",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "aioexabgp": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": false,
-      "git_clone_url": "https://github.com/cooperlees/aioexabgp.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "attrs": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/python-attrs/attrs.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "bandersnatch": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/pypa/bandersnatch.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "channels": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/django/channels.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "cpython": {
-      "disabled": true,
-      "disabled_reason": "To big / slow for GitHub Actions but handy to keep config to use manually or in some other CI in the future",
-      "base_path": "Lib",
-      "cli_arguments": [
-        "--experimental-string-processing",
-        "--extend-exclude",
-        [
-          "Lib/lib2to3/tests/data/different_encoding.py",
-          "|Lib/lib2to3/tests/data/false_encoding.py",
-          "|Lib/lib2to3/tests/data/py2_test_grammar.py",
-          "|Lib/test/bad_coding.py",
-          "|Lib/test/bad_coding2.py",
-          "|Lib/test/badsyntax_3131.py",
-          "|Lib/test/badsyntax_pep3120.py",
-          "|Lib/test/test_base64.py",
-          "|Lib/test/test_exceptions.py",
-          "|Lib/test/test_grammar.py",
-          "|Lib/test/test_named_expressions.py",
-          "|Lib/test/test_patma.py",
-          "|Lib/test/test_tokenize.py",
-          "|Lib/test/test_xml_etree.py",
-          "|Lib/traceback.py"
-        ]
-      ],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/python/cpython.git",
-      "long_checkout": false,
-      "py_versions": ["3.9", "3.10"],
-      "timeout_seconds": 900
-    },
-    "django": {
-      "cli_arguments": [
-        "--experimental-string-processing",
-        "--skip-string-normalization",
-        "--extend-exclude",
-        "/((docs|scripts)/|django/forms/models.py|tests/gis_tests/test_spatialrefsys.py|tests/test_runner_apps/tagged/tests_syntax_error.py)"
-      ],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/django/django.git",
-      "long_checkout": false,
-      "py_versions": ["3.8", "3.9"]
-    },
-    "flake8-bugbear": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": false,
-      "git_clone_url": "https://github.com/PyCQA/flake8-bugbear.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "hypothesis": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/HypothesisWorks/hypothesis.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "pandas": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/pandas-dev/pandas.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "pillow": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/python-pillow/Pillow.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "poetry": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/python-poetry/poetry.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "pyanalyze": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": false,
-      "git_clone_url": "https://github.com/quora/pyanalyze.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "pyramid": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/Pylons/pyramid.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "ptr": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": false,
-      "git_clone_url": "https://github.com/facebookincubator/ptr.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "pytest": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/pytest-dev/pytest.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "scikit-lego": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/koaning/scikit-lego",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "sqlalchemy": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/sqlalchemy/sqlalchemy.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "tox": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/tox-dev/tox.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "typeshed": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/python/typeshed.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "virtualenv": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/pypa/virtualenv.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    },
-    "warehouse": {
-      "cli_arguments": ["--experimental-string-processing"],
-      "expect_formatting_changes": true,
-      "git_clone_url": "https://github.com/pypa/warehouse.git",
-      "long_checkout": false,
-      "py_versions": ["all"]
-    }
-  }
-}
index cc966404a743a37bd2059c89732e7ad9d3b8b384..972f24181cb60e538e5237ea9597c9315758dc21 100644 (file)
@@ -1,13 +1,14 @@
 import asyncio
 import logging
 from concurrent.futures import Executor, ProcessPoolExecutor
-from datetime import datetime
+from datetime import datetime, timezone
 from functools import partial
 from multiprocessing import freeze_support
 from typing import Set, Tuple
 
 try:
     from aiohttp import web
+
     from .middlewares import cors
 except ImportError as ie:
     raise ImportError(
@@ -16,11 +17,11 @@ except ImportError as ie:
         + "to obtain aiohttp_cors: `pip install black[d]`"
     ) from None
 
-import black
-from black.concurrency import maybe_install_uvloop
 import click
 
+import black
 from _black_version import version as __version__
+from black.concurrency import maybe_install_uvloop
 
 # This is used internally by tests to shut down the server prematurely
 _stop_signal = asyncio.Event()
@@ -29,8 +30,10 @@ _stop_signal = asyncio.Event()
 PROTOCOL_VERSION_HEADER = "X-Protocol-Version"
 LINE_LENGTH_HEADER = "X-Line-Length"
 PYTHON_VARIANT_HEADER = "X-Python-Variant"
+SKIP_SOURCE_FIRST_LINE = "X-Skip-Source-First-Line"
 SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
 SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma"
+PREVIEW = "X-Preview"
 FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
 DIFF_HEADER = "X-Diff"
 
@@ -38,8 +41,10 @@ BLACK_HEADERS = [
     PROTOCOL_VERSION_HEADER,
     LINE_LENGTH_HEADER,
     PYTHON_VARIANT_HEADER,
+    SKIP_SOURCE_FIRST_LINE,
     SKIP_STRING_NORMALIZATION_HEADER,
     SKIP_MAGIC_TRAILING_COMMA,
+    PREVIEW,
     FAST_OR_SAFE_HEADER,
     DIFF_HEADER,
 ]
@@ -54,16 +59,24 @@ class InvalidVariantHeader(Exception):
 
 @click.command(context_settings={"help_option_names": ["-h", "--help"]})
 @click.option(
-    "--bind-host", type=str, help="Address to bind the server to.", default="localhost"
+    "--bind-host",
+    type=str,
+    help="Address to bind the server to.",
+    default="localhost",
+    show_default=True,
+)
+@click.option(
+    "--bind-port", type=int, help="Port to listen on", default=45484, show_default=True
 )
-@click.option("--bind-port", type=int, help="Port to listen on", default=45484)
 @click.version_option(version=black.__version__)
 def main(bind_host: str, bind_port: int) -> None:
     logging.basicConfig(level=logging.INFO)
     app = make_app()
     ver = black.__version__
     black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}")
-    web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None)
+    # TODO: aiohttp had an incorrect annotation for `print` argument,
+    #  It'll be fixed once aiohttp releases that code
+    web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None)  # type: ignore[arg-type]
 
 
 def make_app() -> web.Application:
@@ -108,6 +121,10 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
         skip_magic_trailing_comma = bool(
             request.headers.get(SKIP_MAGIC_TRAILING_COMMA, False)
         )
+        skip_source_first_line = bool(
+            request.headers.get(SKIP_SOURCE_FIRST_LINE, False)
+        )
+        preview = bool(request.headers.get(PREVIEW, False))
         fast = False
         if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
             fast = True
@@ -115,25 +132,45 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
             target_versions=versions,
             is_pyi=pyi,
             line_length=line_length,
+            skip_source_first_line=skip_source_first_line,
             string_normalization=not skip_string_normalization,
             magic_trailing_comma=not skip_magic_trailing_comma,
+            preview=preview,
         )
         req_bytes = await request.content.read()
         charset = request.charset if request.charset is not None else "utf8"
         req_str = req_bytes.decode(charset)
-        then = datetime.utcnow()
+        then = datetime.now(timezone.utc)
+
+        header = ""
+        if skip_source_first_line:
+            first_newline_position: int = req_str.find("\n") + 1
+            header = req_str[:first_newline_position]
+            req_str = req_str[first_newline_position:]
 
         loop = asyncio.get_event_loop()
         formatted_str = await loop.run_in_executor(
             executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode)
         )
 
+        # Preserve CRLF line endings
+        nl = req_str.find("\n")
+        if nl > 0 and req_str[nl - 1] == "\r":
+            formatted_str = formatted_str.replace("\n", "\r\n")
+            # If, after swapping line endings, nothing changed, then say so
+            if formatted_str == req_str:
+                raise black.NothingChanged
+
+        # Put the source first line back
+        req_str = header + req_str
+        formatted_str = header + formatted_str
+
         # Only output the diff in the HTTP response
         only_diff = bool(request.headers.get(DIFF_HEADER, False))
         if only_diff:
-            now = datetime.utcnow()
-            src_name = f"In\t{then} +0000"
-            dst_name = f"Out\t{now} +0000"
+            now = datetime.now(timezone.utc)
+            src_name = f"In\t{then}"
+            dst_name = f"Out\t{now}"
             loop = asyncio.get_event_loop()
             formatted_str = await loop.run_in_executor(
                 executor,
@@ -174,10 +211,8 @@ def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersi
                     raise InvalidVariantHeader("major version must be 2 or 3")
                 if len(rest) > 0:
                     minor = int(rest[0])
-                    if major == 2 and minor != 7:
-                        raise InvalidVariantHeader(
-                            "minor version must be 7 for Python 2"
-                        )
+                    if major == 2:
+                        raise InvalidVariantHeader("Python 2 is not supported")
                 else:
                     # Default to lowest supported minor version.
                     minor = 7 if major == 2 else 3
@@ -193,7 +228,6 @@ def parse_python_variant_header(value: str) -> Tuple[bool, Set[black.TargetVersi
 def patched_main() -> None:
     maybe_install_uvloop()
     freeze_support()
-    black.patch_click()
     main()
 
 
diff --git a/.vim/bundle/black/src/blackd/__main__.py b/.vim/bundle/black/src/blackd/__main__.py
new file mode 100644 (file)
index 0000000..b5a4b13
--- /dev/null
@@ -0,0 +1,3 @@
+import blackd
+
+blackd.patched_main()
index 97994ecc1dff81df44d81076534c9886e527bf1f..370e0ae222eebfef1b73c185b4fc6130e87974cc 100644 (file)
@@ -1,7 +1,18 @@
-from typing import Iterable, Awaitable, Callable
-from aiohttp.web_response import StreamResponse
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, TypeVar
+
 from aiohttp.web_request import Request
-from aiohttp.web_middlewares import middleware
+from aiohttp.web_response import StreamResponse
+
+if TYPE_CHECKING:
+    F = TypeVar("F", bound=Callable[..., Any])
+    middleware: Callable[[F], F]
+else:
+    try:
+        from aiohttp.web_middlewares import middleware
+    except ImportError:
+        # @middleware is deprecated and its behaviour is the default since aiohttp 4.0
+        # so if it doesn't exist anymore, define a no-op for forward compatibility.
+        middleware = lambda x: x  # noqa: E731
 
 Handler = Callable[[Request], Awaitable[StreamResponse]]
 Middleware = Callable[[Request, Handler], Awaitable[StreamResponse]]
@@ -31,4 +42,4 @@ def cors(allow_headers: Iterable[str]) -> Middleware:
 
         return resp
 
-    return impl  # type: ignore
+    return impl
index ac8a067378d71ec0ba4f05a2a2af5b2a68fbfad7..5db78723cecd4e4fdd474bb9a528448fc27ea4e4 100644 (file)
@@ -12,11 +12,17 @@ file_input: (NEWLINE | stmt)* ENDMARKER
 single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
 eval_input: testlist NEWLINE* ENDMARKER
 
+typevar: NAME [':' expr]
+paramspec: '**' NAME
+typevartuple: '*' NAME
+typeparam: typevar | paramspec | typevartuple
+typeparams: '[' typeparam (',' typeparam)* [','] ']'
+
 decorator: '@' namedexpr_test NEWLINE
 decorators: decorator+
 decorated: decorators (classdef | funcdef | async_funcdef)
 async_funcdef: ASYNC funcdef
-funcdef: 'def' NAME parameters ['->' test] ':' suite
+funcdef: 'def' NAME [typeparams] parameters ['->' test] ':' suite
 parameters: '(' [typedargslist] ')'
 
 # The following definition for typedarglist is equivalent to this set of rules:
@@ -24,7 +30,7 @@ parameters: '(' [typedargslist] ')'
 #     arguments = argument (',' argument)*
 #     argument = tfpdef ['=' test]
 #     kwargs = '**' tname [',']
-#     args = '*' [tname]
+#     args = '*' [tname_star]
 #     kwonly_kwargs = (',' argument)* [',' [kwargs]]
 #     args_kwonly_kwargs = args kwonly_kwargs | kwargs
 #     poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]]
@@ -34,14 +40,15 @@ parameters: '(' [typedargslist] ')'
 # It needs to be fully expanded to allow our LL(1) parser to work on it.
 
 typedargslist: tfpdef ['=' test] (',' tfpdef ['=' test])* ',' '/' [
-                     ',' [((tfpdef ['=' test] ',')* ('*' [tname] (',' tname ['=' test])*
+                     ',' [((tfpdef ['=' test] ',')* ('*' [tname_star] (',' tname ['=' test])*
                             [',' ['**' tname [',']]] | '**' tname [','])
                      | tfpdef ['=' test] (',' tfpdef ['=' test])* [','])]
-                ] | ((tfpdef ['=' test] ',')* ('*' [tname] (',' tname ['=' test])*
+                ] | ((tfpdef ['=' test] ',')* ('*' [tname_star] (',' tname ['=' test])*
                      [',' ['**' tname [',']]] | '**' tname [','])
                      | tfpdef ['=' test] (',' tfpdef ['=' test])* [','])
 
 tname: NAME [':' test]
+tname_star: NAME [':' (test|star_expr)]
 tfpdef: tname | '(' tfplist ')'
 tfplist: tfpdef (',' tfpdef)* [',']
 
@@ -73,8 +80,8 @@ vfplist: vfpdef (',' vfpdef)* [',']
 
 stmt: simple_stmt | compound_stmt
 simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
-small_stmt: (expr_stmt | print_stmt  | del_stmt | pass_stmt | flow_stmt |
-             import_stmt | global_stmt | exec_stmt | assert_stmt)
+small_stmt: (type_stmt | expr_stmt | del_stmt | pass_stmt | flow_stmt |
+             import_stmt | global_stmt | assert_stmt)
 expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                      ('=' (yield_expr|testlist_star_expr))*)
 annassign: ':' test ['=' (yield_expr|testlist_star_expr)]
@@ -82,8 +89,6 @@ testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
 augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
             '<<=' | '>>=' | '**=' | '//=')
 # For normal and annotated assignments, additional restrictions enforced by the interpreter
-print_stmt: 'print' ( [ test (',' test)* [','] ] |
-                      '>>' test [ (',' test)+ [','] ] )
 del_stmt: 'del' exprlist
 pass_stmt: 'pass'
 flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
@@ -102,24 +107,23 @@ import_as_names: import_as_name (',' import_as_name)* [',']
 dotted_as_names: dotted_as_name (',' dotted_as_name)*
 dotted_name: NAME ('.' NAME)*
 global_stmt: ('global' | 'nonlocal') NAME (',' NAME)*
-exec_stmt: 'exec' expr ['in' test [',' test]]
 assert_stmt: 'assert' test [',' test]
+type_stmt: "type" NAME [typeparams] '=' test
 
-compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt
+compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt | match_stmt
 async_stmt: ASYNC (funcdef | with_stmt | for_stmt)
 if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite)* ['else' ':' suite]
 while_stmt: 'while' namedexpr_test ':' suite ['else' ':' suite]
-for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
+for_stmt: 'for' exprlist 'in' testlist_star_expr ':' suite ['else' ':' suite]
 try_stmt: ('try' ':' suite
            ((except_clause ':' suite)+
            ['else' ':' suite]
            ['finally' ':' suite] |
           'finally' ':' suite))
-with_stmt: 'with' with_item (',' with_item)*  ':' suite
-with_item: test ['as' expr]
-with_var: 'as' expr
+with_stmt: 'with' asexpr_test (',' asexpr_test)*  ':' suite
+
 # NB compile.c makes sure that the default except clause is last
-except_clause: 'except' [test [(',' | 'as') test]]
+except_clause: 'except' ['*'] [test [(',' | 'as') test]]
 suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
 
 # Backward compatibility cruft to support:
@@ -131,7 +135,15 @@ testlist_safe: old_test [(',' old_test)+ [',']]
 old_test: or_test | old_lambdef
 old_lambdef: 'lambda' [varargslist] ':' old_test
 
-namedexpr_test: test [':=' test]
+namedexpr_test: asexpr_test [':=' asexpr_test]
+
+# This is actually not a real rule, though since the parser is very
+# limited in terms of the strategy about match/case rules, we are inserting
+# a virtual case (<expr> as <expr>) as a valid expression. Unless a better
+# approach is thought, the only side effect of this seem to be just allowing
+# more stuff to be parser (which would fail on the ast).
+asexpr_test: test ['as' test]
+
 test: or_test ['if' or_test 'else' test] | lambdef
 or_test: and_test ('or' and_test)*
 and_test: not_test ('and' not_test)*
@@ -156,17 +168,17 @@ listmaker: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star
 testlist_gexp: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
 lambdef: 'lambda' [varargslist] ':' test
 trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
-subscriptlist: subscript (',' subscript)* [',']
+subscriptlist: (subscript|star_expr) (',' (subscript|star_expr))* [',']
 subscript: test [':=' test] | [test] ':' [test] [sliceop]
 sliceop: ':' [test]
 exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
 testlist: test (',' test)* [',']
-dictsetmaker: ( ((test ':' test | '**' expr)
-                 (comp_for | (',' (test ':' test | '**' expr))* [','])) |
+dictsetmaker: ( ((test ':' asexpr_test | '**' expr)
+                 (comp_for | (',' (test ':' asexpr_test | '**' expr))* [','])) |
                 ((test [':=' test] | star_expr)
                 (comp_for | (',' (test [':=' test] | star_expr))* [','])) )
 
-classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
+classdef: 'class' NAME [typeparams] ['(' [arglist] ')'] ':' suite
 
 arglist: argument (',' argument)* [',']
 
@@ -178,8 +190,9 @@ arglist: argument (',' argument)* [',']
 # multiple (test comp_for) arguments are blocked; keyword unpackings
 # that precede iterable unpackings are blocked; etc.
 argument: ( test [comp_for] |
-            test ':=' test |
-            test '=' test |
+            test ':=' test [comp_for] |
+            test 'as' test |
+            test '=' asexpr_test |
            '**' test |
             '*' test )
 
@@ -213,3 +226,31 @@ encoding_decl: NAME
 
 yield_expr: 'yield' [yield_arg]
 yield_arg: 'from' test | testlist_star_expr
+
+
+# 3.10 match statement definition
+
+# PS: normally the grammar is much much more restricted, but
+# at this moment for not trying to bother much with encoding the
+# exact same DSL in a LL(1) parser, we will just accept an expression
+# and let the ast.parse() step of the safe mode to reject invalid
+# grammar.
+
+# The reason why it is more restricted is that, patterns are some
+# sort of a DSL (more advanced than our LHS on assignments, but
+# still in a very limited python subset). They are not really
+# expressions, but who cares. If we can parse them, that is enough
+# to reformat them.
+
+match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
+
+# This is more permissive than the actual version. For example it
+# accepts `match *something:`, even though single-item starred expressions
+# are forbidden.
+subject_expr: (namedexpr_test|star_expr) (',' (namedexpr_test|star_expr))* [',']
+
+# cases
+case_block: "case" patterns [guard] ':' suite
+guard: 'if' namedexpr_test
+patterns: pattern (',' pattern)* [',']
+pattern: (expr|star_expr) ['as' expr]
index ccad28337b697b3344bf323d2e7409784ad9e5b0..38b04158ddb70070110e5047aab88bb9009ea003 100644 (file)
@@ -1,21 +1,24 @@
-A subset of lib2to3 taken from Python 3.7.0b2.
-Commit hash: 9c17e3a1987004b8bcfbe423953aad84493a7984
+A subset of lib2to3 taken from Python 3.7.0b2. Commit hash:
+9c17e3a1987004b8bcfbe423953aad84493a7984
 
 Reasons for forking:
+
 - consistent handling of f-strings for users of Python < 3.6.2
-- backport of BPO-33064 that fixes parsing files with trailing commas after
-  *args and **kwargs
-- backport of GH-6143 that restores the ability to reformat legacy usage of
-  `async`
+- backport of BPO-33064 that fixes parsing files with trailing commas after \*args and
+  \*\*kwargs
+- backport of GH-6143 that restores the ability to reformat legacy usage of `async`
 - support all types of string literals
 - better ability to debug (better reprs)
 - INDENT and DEDENT don't hold whitespace and comment prefixes
 - ability to Cythonize
 
 Change Log:
+
 - Changes default logger used by Driver
 - Backported the following upstream parser changes:
   - "bpo-42381: Allow walrus in set literals and set comprehensions (GH-23332)"
     https://github.com/python/cpython/commit/cae60187cf7a7b26281d012e1952fafe4e2e97e9
   - "bpo-42316: Allow unparenthesized walrus operator in indexes (GH-23317)"
     https://github.com/python/cpython/commit/b0aba1fcdc3da952698d99aec2334faa79a8b68c
+- Tweaks to help mypyc compile faster code (including inlining type information,
+  "Final-ing", etc.)
index 78165217a1b0ebb777c522a56a480c51278aa53f..04eccfa1d4b6ac10b3d264b8fff0b494b8849549 100644 (file)
@@ -29,7 +29,7 @@ without having to invoke the Python pgen C program.
 """
 
 # Python imports
-import regex as re
+import re
 
 # Local imports
 from pgen2 import grammar, token
@@ -63,7 +63,7 @@ class Converter(grammar.Grammar):
         try:
             f = open(filename)
         except OSError as err:
-            print("Can't open %s: %s" % (filename, err))
+            print(f"Can't open {filename}: {err}")
             return False
         self.symbol2number = {}
         self.number2symbol = {}
@@ -72,7 +72,7 @@ class Converter(grammar.Grammar):
             lineno += 1
             mo = re.match(r"^#define\s+(\w+)\s+(\d+)$", line)
             if not mo and line.strip():
-                print("%s(%s): can't parse %s" % (filename, lineno, line.strip()))
+                print(f"{filename}({lineno}): can't parse {line.strip()}")
             else:
                 symbol, number = mo.groups()
                 number = int(number)
@@ -113,7 +113,7 @@ class Converter(grammar.Grammar):
         try:
             f = open(filename)
         except OSError as err:
-            print("Can't open %s: %s" % (filename, err))
+            print(f"Can't open {filename}: {err}")
             return False
         # The code below essentially uses f's iterator-ness!
         lineno = 0
index af1dc6b8aebe032b701df42e9a9ff11257cf3dc4..e629843f8b986ea792d59a150ff64018d2108bcb 100644 (file)
@@ -17,54 +17,115 @@ __all__ = ["Driver", "load_grammar"]
 
 # Python imports
 import io
-import os
 import logging
+import os
 import pkgutil
 import sys
-from typing import (
-    Any,
-    IO,
-    Iterable,
-    List,
-    Optional,
-    Text,
-    Tuple,
-    Union,
-)
-
-# Pgen imports
-from . import grammar, parse, token, tokenize, pgen
+from contextlib import contextmanager
+from dataclasses import dataclass, field
 from logging import Logger
-from blib2to3.pytree import _Convert, NL
+from typing import IO, Any, Iterable, Iterator, List, Optional, Tuple, Union, cast
+
 from blib2to3.pgen2.grammar import Grammar
+from blib2to3.pgen2.tokenize import GoodTokenInfo
+from blib2to3.pytree import NL
+
+# Pgen imports
+from . import grammar, parse, pgen, token, tokenize
 
 Path = Union[str, "os.PathLike[str]"]
 
 
-class Driver(object):
-    def __init__(
-        self,
-        grammar: Grammar,
-        convert: Optional[_Convert] = None,
-        logger: Optional[Logger] = None,
-    ) -> None:
+@dataclass
+class ReleaseRange:
+    start: int
+    end: Optional[int] = None
+    tokens: List[Any] = field(default_factory=list)
+
+    def lock(self) -> None:
+        total_eaten = len(self.tokens)
+        self.end = self.start + total_eaten
+
+
+class TokenProxy:
+    def __init__(self, generator: Any) -> None:
+        self._tokens = generator
+        self._counter = 0
+        self._release_ranges: List[ReleaseRange] = []
+
+    @contextmanager
+    def release(self) -> Iterator["TokenProxy"]:
+        release_range = ReleaseRange(self._counter)
+        self._release_ranges.append(release_range)
+        try:
+            yield self
+        finally:
+            # Lock the last release range to the final position that
+            # has been eaten.
+            release_range.lock()
+
+    def eat(self, point: int) -> Any:
+        eaten_tokens = self._release_ranges[-1].tokens
+        if point < len(eaten_tokens):
+            return eaten_tokens[point]
+        else:
+            while point >= len(eaten_tokens):
+                token = next(self._tokens)
+                eaten_tokens.append(token)
+            return token
+
+    def __iter__(self) -> "TokenProxy":
+        return self
+
+    def __next__(self) -> Any:
+        # If the current position is already compromised (looked up)
+        # return the eaten token, if not just go further on the given
+        # token producer.
+        for release_range in self._release_ranges:
+            assert release_range.end is not None
+
+            start, end = release_range.start, release_range.end
+            if start <= self._counter < end:
+                token = release_range.tokens[self._counter - start]
+                break
+        else:
+            token = next(self._tokens)
+        self._counter += 1
+        return token
+
+    def can_advance(self, to: int) -> bool:
+        # Try to eat, fail if it can't. The eat operation is cached
+        # so there won't be any additional cost of eating here
+        try:
+            self.eat(to)
+        except StopIteration:
+            return False
+        else:
+            return True
+
+
+class Driver:
+    def __init__(self, grammar: Grammar, logger: Optional[Logger] = None) -> None:
         self.grammar = grammar
         if logger is None:
             logger = logging.getLogger(__name__)
         self.logger = logger
-        self.convert = convert
 
-    def parse_tokens(self, tokens: Iterable[Any], debug: bool = False) -> NL:
+    def parse_tokens(self, tokens: Iterable[GoodTokenInfo], debug: bool = False) -> NL:
         """Parse a series of tokens and return the syntax tree."""
         # XXX Move the prefix computation into a wrapper around tokenize.
-        p = parse.Parser(self.grammar, self.convert)
-        p.setup()
+        proxy = TokenProxy(tokens)
+
+        p = parse.Parser(self.grammar)
+        p.setup(proxy=proxy)
+
         lineno = 1
         column = 0
-        indent_columns = []
+        indent_columns: List[int] = []
         type = value = start = end = line_text = None
         prefix = ""
-        for quintuple in tokens:
+
+        for quintuple in proxy:
             type, value, start, end, line_text = quintuple
             if start != (lineno, column):
                 assert (lineno, column) <= start, ((lineno, column), start)
@@ -86,6 +147,7 @@ class Driver(object):
             if type == token.OP:
                 type = grammar.opmap[value]
             if debug:
+                assert type is not None
                 self.logger.debug(
                     "%s %r (prefix=%r)", token.tok_name[type], value, prefix
                 )
@@ -97,7 +159,7 @@ class Driver(object):
             elif type == token.DEDENT:
                 _indent_col = indent_columns.pop()
                 prefix, _prefix = self._partially_consume_prefix(prefix, _indent_col)
-            if p.addtoken(type, value, (prefix, start)):
+            if p.addtoken(cast(int, type), value, (prefix, start)):
                 if debug:
                     self.logger.debug("Stop.")
                 break
@@ -115,30 +177,30 @@ class Driver(object):
         assert p.rootnode is not None
         return p.rootnode
 
-    def parse_stream_raw(self, stream: IO[Text], debug: bool = False) -> NL:
+    def parse_stream_raw(self, stream: IO[str], debug: bool = False) -> NL:
         """Parse a stream and return the syntax tree."""
         tokens = tokenize.generate_tokens(stream.readline, grammar=self.grammar)
         return self.parse_tokens(tokens, debug)
 
-    def parse_stream(self, stream: IO[Text], debug: bool = False) -> NL:
+    def parse_stream(self, stream: IO[str], debug: bool = False) -> NL:
         """Parse a stream and return the syntax tree."""
         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[str] = None, debug: bool = False
     ) -> NL:
         """Parse a file and return the syntax tree."""
-        with io.open(filename, "r", encoding=encoding) as stream:
+        with open(filename, encoding=encoding) as stream:
             return self.parse_stream(stream, debug)
 
-    def parse_string(self, text: Text, debug: bool = False) -> NL:
+    def parse_string(self, text: str, debug: bool = False) -> NL:
         """Parse a string and return the syntax tree."""
         tokens = tokenize.generate_tokens(
             io.StringIO(text).readline, grammar=self.grammar
         )
         return self.parse_tokens(tokens, debug)
 
-    def _partially_consume_prefix(self, prefix: Text, column: int) -> Tuple[Text, Text]:
+    def _partially_consume_prefix(self, prefix: str, column: int) -> Tuple[str, str]:
         lines: List[str] = []
         current_line = ""
         current_column = 0
@@ -166,7 +228,7 @@ class Driver(object):
         return "".join(lines), current_line
 
 
-def _generate_pickle_name(gt: Path, cache_dir: Optional[Path] = None) -> Text:
+def _generate_pickle_name(gt: Path, cache_dir: Optional[Path] = None) -> str:
     head, tail = os.path.splitext(gt)
     if tail == ".txt":
         tail = ""
@@ -178,8 +240,8 @@ def _generate_pickle_name(gt: Path, cache_dir: Optional[Path] = None) -> Text:
 
 
 def load_grammar(
-    gt: Text = "Grammar.txt",
-    gp: Optional[Text] = None,
+    gt: str = "Grammar.txt",
+    gp: Optional[str] = None,
     save: bool = True,
     force: bool = False,
     logger: Optional[Logger] = None,
@@ -189,21 +251,20 @@ def load_grammar(
         logger = logging.getLogger(__name__)
     gp = _generate_pickle_name(gt) if gp is None else gp
     if force or not _newer(gp, gt):
-        logger.info("Generating grammar tables from %s", gt)
         g: grammar.Grammar = pgen.generate_grammar(gt)
         if save:
-            logger.info("Writing grammar tables to %s", gp)
             try:
                 g.dump(gp)
-            except OSError as e:
-                logger.info("Writing failed: %s", e)
+            except OSError:
+                # Ignore error, caching is not vital.
+                pass
     else:
         g = grammar.Grammar()
         g.load(gp)
     return g
 
 
-def _newer(a: Text, b: Text) -> bool:
+def _newer(a: str, b: str) -> bool:
     """Inquire whether file a was written since file b."""
     if not os.path.exists(a):
         return False
@@ -213,7 +274,7 @@ def _newer(a: Text, b: Text) -> bool:
 
 
 def load_packaged_grammar(
-    package: str, grammar_source: Text, cache_dir: Optional[Path] = None
+    package: str, grammar_source: str, cache_dir: Optional[Path] = None
 ) -> grammar.Grammar:
     """Normally, loads a pickled grammar by doing
         pkgutil.get_data(package, pickled_grammar)
@@ -236,7 +297,7 @@ def load_packaged_grammar(
     return g
 
 
-def main(*args: Text) -> bool:
+def main(*args: str) -> bool:
     """Main program, when run as a script: produce grammar pickle files.
 
     Calls load_grammar for each argument, a path to a grammar text file.
index 2882cdac89b2ac138151edd8d1791ea55db799f0..1f3fdc55b972e1c75d0e4bca952c2c0b0d86fece 100644 (file)
@@ -16,19 +16,19 @@ fallback token code OP, but the parser needs the actual token code.
 import os
 import pickle
 import tempfile
-from typing import Any, Dict, List, Optional, Text, Tuple, TypeVar, Union
+from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union
 
 # Local imports
 from . import token
 
 _P = TypeVar("_P", bound="Grammar")
-Label = Tuple[int, Optional[Text]]
+Label = Tuple[int, Optional[str]]
 DFA = List[List[Tuple[int, int]]]
 DFAS = Tuple[DFA, Dict[int, int]]
 Path = Union[str, "os.PathLike[str]"]
 
 
-class Grammar(object):
+class Grammar:
     """Pgen parsing tables conversion class.
 
     Once initialized, this class supplies the grammar tables for the
@@ -89,8 +89,10 @@ class Grammar(object):
         self.dfas: Dict[int, DFAS] = {}
         self.labels: List[Label] = [(0, "EMPTY")]
         self.keywords: Dict[str, int] = {}
+        self.soft_keywords: Dict[str, int] = {}
         self.tokens: Dict[int, int] = {}
         self.symbol2label: Dict[str, int] = {}
+        self.version: Tuple[int, int] = (0, 0)
         self.start = 256
         # Python 3.7+ parses async as a keyword, not an identifier
         self.async_keywords = False
@@ -136,6 +138,7 @@ class Grammar(object):
             "number2symbol",
             "dfas",
             "keywords",
+            "soft_keywords",
             "tokens",
             "symbol2label",
         ):
@@ -143,6 +146,7 @@ class Grammar(object):
         new.labels = self.labels[:]
         new.states = self.states[:]
         new.start = self.start
+        new.version = self.version
         new.async_keywords = self.async_keywords
         return new
 
index b5fe42851148e7fd6270ae728a0327f1523dd4b5..53c0b8ac2bbb2984a8b78d03261d8c7ebd0eb5ca 100644 (file)
@@ -4,11 +4,9 @@
 """Safely evaluate Python string literals without using eval()."""
 
 import re
+from typing import Dict, Match
 
-from typing import Dict, Match, Text
-
-
-simple_escapes: Dict[Text, Text] = {
+simple_escapes: Dict[str, str] = {
     "a": "\a",
     "b": "\b",
     "f": "\f",
@@ -22,7 +20,7 @@ simple_escapes: Dict[Text, Text] = {
 }
 
 
-def escape(m: Match[Text]) -> Text:
+def escape(m: Match[str]) -> str:
     all, tail = m.group(0, 1)
     assert all.startswith("\\")
     esc = simple_escapes.get(tail)
@@ -44,7 +42,7 @@ def escape(m: Match[Text]) -> Text:
     return chr(i)
 
 
-def evalString(s: Text) -> Text:
+def evalString(s: str) -> str:
     assert s.startswith("'") or s.startswith('"'), repr(s[:1])
     q = s[0]
     if s[:3] == q * 3:
index 47c8f02b4f5c8ee081ed70f4ba26f351483095a0..ad51a3dad08dfbbe1fee7cc9b7845eeb7cb263fd 100644 (file)
@@ -9,24 +9,32 @@ See Parser/parser.c in the Python distribution for additional info on
 how this parsing engine works.
 
 """
-
-# Local imports
-from . import token
+from contextlib import contextmanager
 from typing import (
-    Optional,
-    Text,
-    Union,
-    Tuple,
+    TYPE_CHECKING,
+    Any,
+    Callable,
     Dict,
+    Iterator,
     List,
-    Callable,
+    Optional,
     Set,
+    Tuple,
+    Union,
+    cast,
 )
+
 from blib2to3.pgen2.grammar import Grammar
-from blib2to3.pytree import NL, Context, RawNode, Leaf, Node
+from blib2to3.pytree import NL, Context, Leaf, Node, RawNode, convert
+
+# Local imports
+from . import grammar, token, tokenize
+
+if TYPE_CHECKING:
+    from blib2to3.pgen2.driver import TokenProxy
 
 
-Results = Dict[Text, NL]
+Results = Dict[str, NL]
 Convert = Callable[[Grammar, RawNode], Union[Node, Leaf]]
 DFA = List[List[Tuple[int, int]]]
 DFAS = Tuple[DFA, Dict[int, int]]
@@ -37,14 +45,97 @@ def lam_sub(grammar: Grammar, node: RawNode) -> NL:
     return Node(type=node[0], children=node[3], context=node[2])
 
 
+# A placeholder node, used when parser is backtracking.
+DUMMY_NODE = (-1, None, None, None)
+
+
+def stack_copy(
+    stack: List[Tuple[DFAS, int, RawNode]]
+) -> List[Tuple[DFAS, int, RawNode]]:
+    """Nodeless stack copy."""
+    return [(dfa, label, DUMMY_NODE) for dfa, label, _ in stack]
+
+
+class Recorder:
+    def __init__(self, parser: "Parser", ilabels: List[int], context: Context) -> None:
+        self.parser = parser
+        self._ilabels = ilabels
+        self.context = context  # not really matter
+
+        self._dead_ilabels: Set[int] = set()
+        self._start_point = self.parser.stack
+        self._points = {ilabel: stack_copy(self._start_point) for ilabel in ilabels}
+
+    @property
+    def ilabels(self) -> Set[int]:
+        return self._dead_ilabels.symmetric_difference(self._ilabels)
+
+    @contextmanager
+    def switch_to(self, ilabel: int) -> Iterator[None]:
+        with self.backtrack():
+            self.parser.stack = self._points[ilabel]
+            try:
+                yield
+            except ParseError:
+                self._dead_ilabels.add(ilabel)
+            finally:
+                self.parser.stack = self._start_point
+
+    @contextmanager
+    def backtrack(self) -> Iterator[None]:
+        """
+        Use the node-level invariant ones for basic parsing operations (push/pop/shift).
+        These still will operate on the stack; but they won't create any new nodes, or
+        modify the contents of any other existing nodes.
+
+        This saves us a ton of time when we are backtracking, since we
+        want to restore to the initial state as quick as possible, which
+        can only be done by having as little mutatations as possible.
+        """
+        is_backtracking = self.parser.is_backtracking
+        try:
+            self.parser.is_backtracking = True
+            yield
+        finally:
+            self.parser.is_backtracking = is_backtracking
+
+    def add_token(self, tok_type: int, tok_val: str, raw: bool = False) -> None:
+        func: Callable[..., Any]
+        if raw:
+            func = self.parser._addtoken
+        else:
+            func = self.parser.addtoken
+
+        for ilabel in self.ilabels:
+            with self.switch_to(ilabel):
+                args = [tok_type, tok_val, self.context]
+                if raw:
+                    args.insert(0, ilabel)
+                func(*args)
+
+    def determine_route(
+        self, value: Optional[str] = None, force: bool = False
+    ) -> Optional[int]:
+        alive_ilabels = self.ilabels
+        if len(alive_ilabels) == 0:
+            *_, most_successful_ilabel = self._dead_ilabels
+            raise ParseError("bad input", most_successful_ilabel, value, self.context)
+
+        ilabel, *rest = alive_ilabels
+        if force or not rest:
+            return ilabel
+        else:
+            return None
+
+
 class ParseError(Exception):
     """Exception to signal the parser is stuck."""
 
     def __init__(
-        self, msg: Text, type: Optional[int], value: Optional[Text], context: Context
+        self, msg: str, type: Optional[int], value: Optional[str], context: Context
     ) -> None:
         Exception.__init__(
-            self, "%s: type=%r, value=%r, context=%r" % (msg, type, value, context)
+            self, f"{msg}: type={type!r}, value={value!r}, context={context!r}"
         )
         self.msg = msg
         self.type = type
@@ -52,7 +143,7 @@ class ParseError(Exception):
         self.context = context
 
 
-class Parser(object):
+class Parser:
     """Parser engine.
 
     The proper usage sequence is:
@@ -100,6 +191,11 @@ class Parser(object):
         to be converted.  The syntax tree is converted from the bottom
         up.
 
+        **post-note: the convert argument is ignored since for Black's
+        usage, convert will always be blib2to3.pytree.convert. Allowing
+        this to be dynamic hurts mypyc's ability to use early binding.
+        These docs are left for historical and informational value.
+
         A concrete syntax tree node is a (type, value, context, nodes)
         tuple, where type is the node type (a token or symbol number),
         value is None for symbols and a string for tokens, context is
@@ -112,9 +208,12 @@ class Parser(object):
 
         """
         self.grammar = grammar
+        # See note in docstring above. TL;DR this is ignored.
         self.convert = convert or lam_sub
+        self.is_backtracking = False
+        self.last_token: Optional[int] = None
 
-    def setup(self, start: Optional[int] = None) -> None:
+    def setup(self, proxy: "TokenProxy", start: Optional[int] = None) -> None:
         """Prepare for parsing.
 
         This *must* be called before starting to parse.
@@ -137,11 +236,58 @@ class Parser(object):
         self.stack: List[Tuple[DFAS, int, RawNode]] = [stackentry]
         self.rootnode: Optional[NL] = None
         self.used_names: Set[str] = set()
+        self.proxy = proxy
+        self.last_token = None
 
-    def addtoken(self, type: int, value: Optional[Text], context: Context) -> bool:
+    def addtoken(self, type: int, value: str, context: Context) -> bool:
         """Add a token; return True iff this is the end of the program."""
         # Map from token to label
-        ilabel = self.classify(type, value, context)
+        ilabels = self.classify(type, value, context)
+        assert len(ilabels) >= 1
+
+        # If we have only one state to advance, we'll directly
+        # take it as is.
+        if len(ilabels) == 1:
+            [ilabel] = ilabels
+            return self._addtoken(ilabel, type, value, context)
+
+        # If there are multiple states which we can advance (only
+        # happen under soft-keywords), then we will try all of them
+        # in parallel and as soon as one state can reach further than
+        # the rest, we'll choose that one. This is a pretty hacky
+        # and hopefully temporary algorithm.
+        #
+        # For a more detailed explanation, check out this post:
+        # https://tree.science/what-the-backtracking.html
+
+        with self.proxy.release() as proxy:
+            counter, force = 0, False
+            recorder = Recorder(self, ilabels, context)
+            recorder.add_token(type, value, raw=True)
+
+            next_token_value = value
+            while recorder.determine_route(next_token_value) is None:
+                if not proxy.can_advance(counter):
+                    force = True
+                    break
+
+                next_token_type, next_token_value, *_ = proxy.eat(counter)
+                if next_token_type in (tokenize.COMMENT, tokenize.NL):
+                    counter += 1
+                    continue
+
+                if next_token_type == tokenize.OP:
+                    next_token_type = grammar.opmap[next_token_value]
+
+                recorder.add_token(next_token_type, next_token_value)
+                counter += 1
+
+            ilabel = cast(int, recorder.determine_route(next_token_value, force=force))
+            assert ilabel is not None
+
+        return self._addtoken(ilabel, type, value, context)
+
+    def _addtoken(self, ilabel: int, type: int, value: str, context: Context) -> bool:
         # Loop until the token is shifted; may raise exceptions
         while True:
             dfa, state, node = self.stack[-1]
@@ -149,10 +295,18 @@ class Parser(object):
             arcs = states[state]
             # Look for a state with this label
             for i, newstate in arcs:
-                t, v = self.grammar.labels[i]
-                if ilabel == i:
+                t = self.grammar.labels[i][0]
+                if t >= 256:
+                    # See if it's a symbol and if we're in its first set
+                    itsdfa = self.grammar.dfas[t]
+                    itsstates, itsfirst = itsdfa
+                    if ilabel in itsfirst:
+                        # Push a symbol
+                        self.push(t, itsdfa, newstate, context)
+                        break  # To continue the outer while loop
+
+                elif ilabel == i:
                     # Look it up in the list of labels
-                    assert t < 256
                     # Shift a token; we're done with it
                     self.shift(type, value, newstate, context)
                     # Pop while we are in an accept-only state
@@ -165,15 +319,9 @@ class Parser(object):
                         dfa, state, node = self.stack[-1]
                         states, first = dfa
                     # Done with this token
+                    self.last_token = type
                     return False
-                elif t >= 256:
-                    # See if it's a symbol and if we're in its first set
-                    itsdfa = self.grammar.dfas[t]
-                    itsstates, itsfirst = itsdfa
-                    if ilabel in itsfirst:
-                        # Push a symbol
-                        self.push(t, self.grammar.dfas[t], newstate, context)
-                        break  # To continue the outer while loop
+
             else:
                 if (0, state) in arcs:
                     # An accepting state, pop it and try something else
@@ -185,47 +333,75 @@ class Parser(object):
                     # No success finding a transition
                     raise ParseError("bad input", type, value, context)
 
-    def classify(self, type: int, value: Optional[Text], context: Context) -> int:
-        """Turn a token into a label.  (Internal)"""
+    def classify(self, type: int, value: str, context: Context) -> List[int]:
+        """Turn a token into a label.  (Internal)
+
+        Depending on whether the value is a soft-keyword or not,
+        this function may return multiple labels to choose from."""
         if type == token.NAME:
             # Keep a listing of all used names
-            assert value is not None
             self.used_names.add(value)
             # Check for reserved words
-            ilabel = self.grammar.keywords.get(value)
-            if ilabel is not None:
-                return ilabel
+            if value in self.grammar.keywords:
+                return [self.grammar.keywords[value]]
+            elif value in self.grammar.soft_keywords:
+                assert type in self.grammar.tokens
+                # Current soft keywords (match, case, type) can only appear at the
+                # beginning of a statement. So as a shortcut, don't try to treat them
+                # like keywords in any other context.
+                # ('_' is also a soft keyword in the real grammar, but for our grammar
+                # it's just an expression, so we don't need to treat it specially.)
+                if self.last_token not in (
+                    None,
+                    token.INDENT,
+                    token.DEDENT,
+                    token.NEWLINE,
+                    token.SEMI,
+                    token.COLON,
+                ):
+                    return [self.grammar.tokens[type]]
+                return [
+                    self.grammar.tokens[type],
+                    self.grammar.soft_keywords[value],
+                ]
+
         ilabel = self.grammar.tokens.get(type)
         if ilabel is None:
             raise ParseError("bad token", type, value, context)
-        return ilabel
+        return [ilabel]
 
-    def shift(
-        self, type: int, value: Optional[Text], newstate: int, context: Context
-    ) -> None:
+    def shift(self, type: int, value: str, newstate: int, context: Context) -> None:
         """Shift a token.  (Internal)"""
-        dfa, state, node = self.stack[-1]
-        assert value is not None
-        assert context is not None
-        rawnode: RawNode = (type, value, context, None)
-        newnode = self.convert(self.grammar, rawnode)
-        if newnode is not None:
+        if self.is_backtracking:
+            dfa, state, _ = self.stack[-1]
+            self.stack[-1] = (dfa, newstate, DUMMY_NODE)
+        else:
+            dfa, state, node = self.stack[-1]
+            rawnode: RawNode = (type, value, context, None)
+            newnode = convert(self.grammar, rawnode)
             assert node[-1] is not None
             node[-1].append(newnode)
-        self.stack[-1] = (dfa, newstate, node)
+            self.stack[-1] = (dfa, newstate, node)
 
     def push(self, type: int, newdfa: DFAS, newstate: int, context: Context) -> None:
         """Push a nonterminal.  (Internal)"""
-        dfa, state, node = self.stack[-1]
-        newnode: RawNode = (type, None, context, [])
-        self.stack[-1] = (dfa, newstate, node)
-        self.stack.append((newdfa, 0, newnode))
+        if self.is_backtracking:
+            dfa, state, _ = self.stack[-1]
+            self.stack[-1] = (dfa, newstate, DUMMY_NODE)
+            self.stack.append((newdfa, 0, DUMMY_NODE))
+        else:
+            dfa, state, node = self.stack[-1]
+            newnode: RawNode = (type, None, context, [])
+            self.stack[-1] = (dfa, newstate, node)
+            self.stack.append((newdfa, 0, newnode))
 
     def pop(self) -> None:
         """Pop a nonterminal.  (Internal)"""
-        popdfa, popstate, popnode = self.stack.pop()
-        newnode = self.convert(self.grammar, popnode)
-        if newnode is not None:
+        if self.is_backtracking:
+            self.stack.pop()
+        else:
+            popdfa, popstate, popnode = self.stack.pop()
+            newnode = convert(self.grammar, popnode)
             if self.stack:
                 dfa, state, node = self.stack[-1]
                 assert node[-1] is not None
index 564ebbd1184c9738a10ce5752757a1262cee4573..3ece9bb41edd51c7d2d82a317cb14dafcea4f1f4 100644 (file)
@@ -1,26 +1,22 @@
 # Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
 # Licensed to PSF under a Contributor Agreement.
 
-# Pgen imports
-from . import grammar, token, tokenize
-
+import os
 from typing import (
+    IO,
     Any,
     Dict,
-    IO,
     Iterator,
     List,
+    NoReturn,
     Optional,
-    Text,
+    Sequence,
     Tuple,
     Union,
-    Sequence,
-    NoReturn,
 )
-from blib2to3.pgen2 import grammar
-from blib2to3.pgen2.tokenize import GoodTokenInfo
-import os
 
+from blib2to3.pgen2 import grammar, token, tokenize
+from blib2to3.pgen2.tokenize import GoodTokenInfo
 
 Path = Union[str, "os.PathLike[str]"]
 
@@ -29,17 +25,16 @@ class PgenGrammar(grammar.Grammar):
     pass
 
 
-class ParserGenerator(object):
-
+class ParserGenerator:
     filename: Path
-    stream: IO[Text]
+    stream: IO[str]
     generator: Iterator[GoodTokenInfo]
-    first: Dict[Text, Optional[Dict[Text, int]]]
+    first: Dict[str, Optional[Dict[str, int]]]
 
-    def __init__(self, filename: Path, stream: Optional[IO[Text]] = None) -> None:
+    def __init__(self, filename: Path, stream: Optional[IO[str]] = None) -> None:
         close_stream = None
         if stream is None:
-            stream = open(filename)
+            stream = open(filename, encoding="utf-8")
             close_stream = stream.close
         self.filename = filename
         self.stream = stream
@@ -76,7 +71,7 @@ class ParserGenerator(object):
         c.start = c.symbol2number[self.startsymbol]
         return c
 
-    def make_first(self, c: PgenGrammar, name: Text) -> Dict[int, int]:
+    def make_first(self, c: PgenGrammar, name: str) -> Dict[int, int]:
         rawfirst = self.first[name]
         assert rawfirst is not None
         first = {}
@@ -86,7 +81,7 @@ class ParserGenerator(object):
             first[ilabel] = 1
         return first
 
-    def make_label(self, c: PgenGrammar, label: Text) -> int:
+    def make_label(self, c: PgenGrammar, label: str) -> int:
         # XXX Maybe this should be a method on a subclass of converter?
         ilabel = len(c.labels)
         if label[0].isalpha():
@@ -115,12 +110,17 @@ class ParserGenerator(object):
             assert label[0] in ('"', "'"), label
             value = eval(label)
             if value[0].isalpha():
+                if label[0] == '"':
+                    keywords = c.soft_keywords
+                else:
+                    keywords = c.keywords
+
                 # A keyword
-                if value in c.keywords:
-                    return c.keywords[value]
+                if value in keywords:
+                    return keywords[value]
                 else:
                     c.labels.append((token.NAME, value))
-                    c.keywords[value] = ilabel
+                    keywords[value] = ilabel
                     return ilabel
             else:
                 # An operator (any non-numeric token)
@@ -140,13 +140,13 @@ class ParserGenerator(object):
                 self.calcfirst(name)
             # print name, self.first[name].keys()
 
-    def calcfirst(self, name: Text) -> None:
+    def calcfirst(self, name: str) -> None:
         dfa = self.dfas[name]
         self.first[name] = None  # dummy to detect left recursion
         state = dfa[0]
         totalset: Dict[str, int] = {}
         overlapcheck = {}
-        for label, next in state.arcs.items():
+        for label in state.arcs:
             if label in self.dfas:
                 if label in self.first:
                     fset = self.first[label]
@@ -172,7 +172,7 @@ class ParserGenerator(object):
                 inverse[symbol] = label
         self.first[name] = totalset
 
-    def parse(self) -> Tuple[Dict[Text, List["DFAState"]], Text]:
+    def parse(self) -> Tuple[Dict[str, List["DFAState"]], str]:
         dfas = {}
         startsymbol: Optional[str] = None
         # MSTART: (NEWLINE | RULE)* ENDMARKER
@@ -187,9 +187,9 @@ class ParserGenerator(object):
             # self.dump_nfa(name, a, z)
             dfa = self.make_dfa(a, z)
             # self.dump_dfa(name, dfa)
-            oldlen = len(dfa)
+            oldlen = len(dfa)
             self.simplify_dfa(dfa)
-            newlen = len(dfa)
+            newlen = len(dfa)
             dfas[name] = dfa
             # print name, oldlen, newlen
             if startsymbol is None:
@@ -236,7 +236,7 @@ class ParserGenerator(object):
                 state.addarc(st, label)
         return states  # List of DFAState instances; first one is start
 
-    def dump_nfa(self, name: Text, start: "NFAState", finish: "NFAState") -> None:
+    def dump_nfa(self, name: str, start: "NFAState", finish: "NFAState") -> None:
         print("Dump of NFA for", name)
         todo = [start]
         for i, state in enumerate(todo):
@@ -252,7 +252,7 @@ class ParserGenerator(object):
                 else:
                     print("    %s -> %d" % (label, j))
 
-    def dump_dfa(self, name: Text, dfa: Sequence["DFAState"]) -> None:
+    def dump_dfa(self, name: str, dfa: Sequence["DFAState"]) -> None:
         print("Dump of DFA for", name)
         for i, state in enumerate(dfa):
             print("  State", i, state.isfinal and "(final)" or "")
@@ -343,9 +343,9 @@ class ParserGenerator(object):
             self.raise_error(
                 "expected (...) or NAME or STRING, got %s/%s", self.type, self.value
             )
-            assert False
+            raise AssertionError
 
-    def expect(self, type: int, value: Optional[Any] = None) -> Text:
+    def expect(self, type: int, value: Optional[Any] = None) -> str:
         if self.type != type or (value is not None and self.value != value):
             self.raise_error(
                 "expected %s/%s, got %s/%s", type, value, self.type, self.value
@@ -365,27 +365,27 @@ class ParserGenerator(object):
         if args:
             try:
                 msg = msg % args
-            except:
+            except Exception:
                 msg = " ".join([msg] + list(map(str, args)))
         raise SyntaxError(msg, (self.filename, self.end[0], self.end[1], self.line))
 
 
-class NFAState(object):
-    arcs: List[Tuple[Optional[Text], "NFAState"]]
+class NFAState:
+    arcs: List[Tuple[Optional[str], "NFAState"]]
 
     def __init__(self) -> None:
         self.arcs = []  # list of (label, NFAState) pairs
 
-    def addarc(self, next: "NFAState", label: Optional[Text] = None) -> None:
+    def addarc(self, next: "NFAState", label: Optional[str] = None) -> None:
         assert label is None or isinstance(label, str)
         assert isinstance(next, NFAState)
         self.arcs.append((label, next))
 
 
-class DFAState(object):
+class DFAState:
     nfaset: Dict[NFAState, Any]
     isfinal: bool
-    arcs: Dict[Text, "DFAState"]
+    arcs: Dict[str, "DFAState"]
 
     def __init__(self, nfaset: Dict[NFAState, Any], final: NFAState) -> None:
         assert isinstance(nfaset, dict)
@@ -395,7 +395,7 @@ class DFAState(object):
         self.isfinal = final in nfaset
         self.arcs = {}  # map from label to DFAState
 
-    def addarc(self, next: "DFAState", label: Text) -> None:
+    def addarc(self, next: "DFAState", label: str) -> None:
         assert isinstance(label, str)
         assert label not in self.arcs
         assert isinstance(next, DFAState)
index 1e0dec9c714d9c5cf518e863e1087761f77a5253..ed2fc4e85fce976b972d7f69b038cb2270100670 100644 (file)
@@ -1,12 +1,6 @@
 """Token constants (from "token.h")."""
 
-import sys
-from typing import Dict
-
-if sys.version_info < (3, 8):
-    from typing_extensions import Final
-else:
-    from typing import Final
+from typing import Dict, Final
 
 #  Taken from Python (r53757) and modified to include some tokens
 #   originally monkeypatched in by pgen2.tokenize
@@ -78,7 +72,7 @@ NT_OFFSET: Final = 256
 
 tok_name: Final[Dict[int, str]] = {}
 for _name, _value in list(globals().items()):
-    if type(_value) is type(0):
+    if type(_value) is int:
         tok_name[_value] = _name
 
 
index bad79b2dc2c73d4ecac36b4a573c0d8bc42f9270..d0607f4b1e17b202d4d19603287ed5cfc0a57288 100644 (file)
@@ -27,27 +27,44 @@ are the same, except instead of generating tokens, tokeneater is a callback
 function to which the 5 fields described above are passed as 5 arguments,
 each time a new token is found."""
 
+import sys
 from typing import (
     Callable,
+    Final,
     Iterable,
     Iterator,
     List,
     Optional,
-    Text,
-    Tuple,
     Pattern,
+    Set,
+    Tuple,
     Union,
     cast,
 )
-from blib2to3.pgen2.token import *
+
 from blib2to3.pgen2.grammar import Grammar
+from blib2to3.pgen2.token import (
+    ASYNC,
+    AWAIT,
+    COMMENT,
+    DEDENT,
+    ENDMARKER,
+    ERRORTOKEN,
+    INDENT,
+    NAME,
+    NEWLINE,
+    NL,
+    NUMBER,
+    OP,
+    STRING,
+    tok_name,
+)
 
 __author__ = "Ka-Ping Yee <ping@lfw.org>"
 __credits__ = "GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, Skip Montanaro"
 
-import regex as re
+import re
 from codecs import BOM_UTF8, lookup
-from blib2to3.pgen2.token import *
 
 from . import token
 
@@ -59,27 +76,27 @@ __all__ = [x for x in dir(token) if x[0] != "_"] + [
 del token
 
 
-def group(*choices):
+def group(*choices: str) -> str:
     return "(" + "|".join(choices) + ")"
 
 
-def any(*choices):
+def any(*choices: str) -> str:
     return group(*choices) + "*"
 
 
-def maybe(*choices):
+def maybe(*choices: str) -> str:
     return group(*choices) + "?"
 
 
-def _combinations(*l):
-    return set(x + y for x in l for y in l + ("",) if x.casefold() != y.casefold())
+def _combinations(*l: str) -> Set[str]:
+    return {x + y for x in l for y in l + ("",) if x.casefold() != y.casefold()}
 
 
 Whitespace = r"[ \f\t]*"
 Comment = r"#[^\r\n]*"
 Ignore = Whitespace + any(r"\\\r?\n" + Whitespace) + maybe(Comment)
 Name = (  # this is invalid but it's fine because Name comes after Number in all groups
-    r"\w+"
+    r"[^\s#\(\)\[\]\{\}+\-*/!@$%^&=|;:'\",\.<>/?`~\\]+"
 )
 
 Binnumber = r"0[bB]_?[01]+(?:_[01]+)*"
@@ -139,7 +156,7 @@ ContStr = group(
 PseudoExtras = group(r"\\\r?\n", Comment, Triple)
 PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name)
 
-pseudoprog = re.compile(PseudoToken, re.UNICODE)
+pseudoprog: Final = re.compile(PseudoToken, re.UNICODE)
 single3prog = re.compile(Single3)
 double3prog = re.compile(Double3)
 
@@ -149,22 +166,21 @@ _strprefixes = (
     | {"u", "U", "ur", "uR", "Ur", "UR"}
 )
 
-endprogs = {
+endprogs: Final = {
     "'": re.compile(Single),
     '"': re.compile(Double),
     "'''": single3prog,
     '"""': double3prog,
     **{f"{prefix}'''": single3prog for prefix in _strprefixes},
     **{f'{prefix}"""': double3prog for prefix in _strprefixes},
-    **{prefix: None for prefix in _strprefixes},
 }
 
-triple_quoted = (
+triple_quoted: Final = (
     {"'''", '"""'}
     | {f"{prefix}'''" for prefix in _strprefixes}
     | {f'{prefix}"""' for prefix in _strprefixes}
 )
-single_quoted = (
+single_quoted: Final = (
     {"'", '"'}
     | {f"{prefix}'" for prefix in _strprefixes}
     | {f'{prefix}"' for prefix in _strprefixes}
@@ -181,19 +197,23 @@ class StopTokenizing(Exception):
     pass
 
 
-def printtoken(type, token, xxx_todo_changeme, xxx_todo_changeme1, line):  # for testing
-    (srow, scol) = xxx_todo_changeme
-    (erow, ecol) = xxx_todo_changeme1
+Coord = Tuple[int, int]
+
+
+def printtoken(
+    type: int, token: str, srow_col: Coord, erow_col: Coord, line: str
+) -> None:  # for testing
+    (srow, scol) = srow_col
+    (erow, ecol) = erow_col
     print(
         "%d,%d-%d,%d:\t%s\t%s" % (srow, scol, erow, ecol, tok_name[type], repr(token))
     )
 
 
-Coord = Tuple[int, int]
-TokenEater = Callable[[int, Text, Coord, Coord, Text], None]
+TokenEater = Callable[[int, str, Coord, Coord, str], None]
 
 
-def tokenize(readline: Callable[[], Text], tokeneater: TokenEater = printtoken) -> None:
+def tokenize(readline: Callable[[], str], tokeneater: TokenEater = printtoken) -> None:
     """
     The tokenize() function accepts two parameters: one representing the
     input stream, and one providing an output mechanism for tokenize().
@@ -213,18 +233,17 @@ def tokenize(readline: Callable[[], Text], tokeneater: TokenEater = printtoken)
 
 
 # backwards compatible interface
-def tokenize_loop(readline, tokeneater):
+def tokenize_loop(readline: Callable[[], str], tokeneater: TokenEater) -> None:
     for token_info in generate_tokens(readline):
         tokeneater(*token_info)
 
 
-GoodTokenInfo = Tuple[int, Text, Coord, Coord, Text]
+GoodTokenInfo = Tuple[int, str, Coord, Coord, str]
 TokenInfo = Union[Tuple[int, str], GoodTokenInfo]
 
 
 class Untokenizer:
-
-    tokens: List[Text]
+    tokens: List[str]
     prev_row: int
     prev_col: int
 
@@ -240,13 +259,13 @@ class Untokenizer:
         if col_offset:
             self.tokens.append(" " * col_offset)
 
-    def untokenize(self, iterable: Iterable[TokenInfo]) -> Text:
+    def untokenize(self, iterable: Iterable[TokenInfo]) -> str:
         for t in iterable:
             if len(t) == 2:
                 self.compat(cast(Tuple[int, str], t), iterable)
                 break
             tok_type, token, start, end, line = cast(
-                Tuple[int, Text, Coord, Coord, Text], t
+                Tuple[int, str, Coord, Coord, str], t
             )
             self.add_whitespace(start)
             self.tokens.append(token)
@@ -256,7 +275,7 @@ class Untokenizer:
                 self.prev_col = 0
         return "".join(self.tokens)
 
-    def compat(self, token: Tuple[int, Text], iterable: Iterable[TokenInfo]) -> None:
+    def compat(self, token: Tuple[int, str], iterable: Iterable[TokenInfo]) -> None:
         startline = False
         indents = []
         toks_append = self.tokens.append
@@ -286,7 +305,7 @@ class Untokenizer:
 
 
 cookie_re = re.compile(r"^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)", re.ASCII)
-blank_re = re.compile(br"^[ \t\f]*(?:[#\r\n]|$)", re.ASCII)
+blank_re = re.compile(rb"^[ \t\f]*(?:[#\r\n]|$)", re.ASCII)
 
 
 def _get_normal_name(orig_enc: str) -> str:
@@ -328,7 +347,7 @@ def detect_encoding(readline: Callable[[], bytes]) -> Tuple[str, List[bytes]]:
         try:
             return readline()
         except StopIteration:
-            return bytes()
+            return b""
 
     def find_cookie(line: bytes) -> Optional[str]:
         try:
@@ -377,7 +396,7 @@ def detect_encoding(readline: Callable[[], bytes]) -> Tuple[str, List[bytes]]:
     return default, [first, second]
 
 
-def untokenize(iterable: Iterable[TokenInfo]) -> Text:
+def untokenize(iterable: Iterable[TokenInfo]) -> str:
     """Transform tokens back into Python source code.
 
     Each element returned by the iterable must be a token sequence
@@ -400,7 +419,7 @@ def untokenize(iterable: Iterable[TokenInfo]) -> Text:
 
 
 def generate_tokens(
-    readline: Callable[[], Text], grammar: Optional[Grammar] = None
+    readline: Callable[[], str], grammar: Optional[Grammar] = None
 ) -> Iterator[GoodTokenInfo]:
     """
     The generate_tokens() generator requires one argument, readline, which
@@ -418,7 +437,7 @@ def generate_tokens(
     logical line; continuation lines are included.
     """
     lnum = parenlev = continued = 0
-    numchars = "0123456789"
+    numchars: Final[str] = "0123456789"
     contstr, needcont = "", 0
     contline: Optional[str] = None
     indents = [0]
@@ -427,7 +446,7 @@ def generate_tokens(
     # `await` as keywords.
     async_keywords = False if grammar is None else grammar.async_keywords
     # 'stashed' and 'async_*' are used for async/await parsing
-    stashed = None
+    stashed: Optional[GoodTokenInfo] = None
     async_def = False
     async_def_indent = 0
     async_def_nl = False
@@ -440,7 +459,7 @@ def generate_tokens(
             line = readline()
         except StopIteration:
             line = ""
-        lnum = lnum + 1
+        lnum += 1
         pos, max = 0, len(line)
 
         if contstr:  # continued string
@@ -481,14 +500,14 @@ def generate_tokens(
             column = 0
             while pos < max:  # measure leading whitespace
                 if line[pos] == " ":
-                    column = column + 1
+                    column += 1
                 elif line[pos] == "\t":
                     column = (column // tabsize + 1) * tabsize
                 elif line[pos] == "\f":
                     column = 0
                 else:
                     break
-                pos = pos + 1
+                pos += 1
             if pos == max:
                 break
 
@@ -507,7 +526,7 @@ def generate_tokens(
                     COMMENT,
                     comment_token,
                     (lnum, pos),
-                    (lnum, pos + len(comment_token)),
+                    (lnum, nl_pos),
                     line,
                 )
                 yield (NL, line[nl_pos:], (lnum, nl_pos), (lnum, len(line)), line)
@@ -592,11 +611,15 @@ def generate_tokens(
                 ):
                     if token[-1] == "\n":  # continued string
                         strstart = (lnum, start)
-                        endprog = (
-                            endprogs[initial]
-                            or endprogs[token[1]]
-                            or endprogs[token[2]]
+                        maybe_endprog = (
+                            endprogs.get(initial)
+                            or endprogs.get(token[1])
+                            or endprogs.get(token[2])
                         )
+                        assert (
+                            maybe_endprog is not None
+                        ), f"endprog not found for {token}"
+                        endprog = maybe_endprog
                         contstr, needcont = line[start:], 1
                         contline = line
                         break
@@ -624,7 +647,6 @@ def generate_tokens(
 
                     if token in ("def", "for"):
                         if stashed and stashed[0] == NAME and stashed[1] == "async":
-
                             if token == "def":
                                 async_def = True
                                 async_def_indent = indents[-1]
@@ -652,29 +674,27 @@ def generate_tokens(
                     continued = 1
                 else:
                     if initial in "([{":
-                        parenlev = parenlev + 1
+                        parenlev += 1
                     elif initial in ")]}":
-                        parenlev = parenlev - 1
+                        parenlev -= 1
                     if stashed:
                         yield stashed
                         stashed = None
                     yield (OP, token, spos, epos, line)
             else:
                 yield (ERRORTOKEN, line[pos], (lnum, pos), (lnum, pos + 1), line)
-                pos = pos + 1
+                pos += 1
 
     if stashed:
         yield stashed
         stashed = None
 
-    for indent in indents[1:]:  # pop remaining indent levels
+    for _indent in indents[1:]:  # pop remaining indent levels
         yield (DEDENT, "", (lnum, 0), (lnum, 0), "")
     yield (ENDMARKER, "", (lnum, 0), (lnum, 0), "")
 
 
 if __name__ == "__main__":  # testing
-    import sys
-
     if len(sys.argv) > 1:
         tokenize(open(sys.argv[1]).readline)
     else:
index b8362b814735afffb1d7a2804dc0361f0fa88a76..2b43b4c112b15c3c508767d6681129457d8a2805 100644 (file)
@@ -5,13 +5,10 @@
 
 # Python imports
 import os
-
 from typing import Union
 
 # Local imports
-from .pgen2 import token
 from .pgen2 import driver
-
 from .pgen2.grammar import Grammar
 
 # Moved into initialize because mypyc can't handle __file__ (XXX bug)
@@ -21,7 +18,7 @@ from .pgen2.grammar import Grammar
 #                                      "PatternGrammar.txt")
 
 
-class Symbols(object):
+class Symbols:
     def __init__(self, grammar: Grammar) -> None:
         """Initializer.
 
@@ -39,12 +36,14 @@ class _python_symbols(Symbols):
     arglist: int
     argument: int
     arith_expr: int
+    asexpr_test: int
     assert_stmt: int
     async_funcdef: int
     async_stmt: int
     atom: int
     augassign: int
     break_stmt: int
+    case_block: int
     classdef: int
     comp_for: int
     comp_if: int
@@ -64,7 +63,6 @@ class _python_symbols(Symbols):
     encoding_decl: int
     eval_input: int
     except_clause: int
-    exec_stmt: int
     expr: int
     expr_stmt: int
     exprlist: int
@@ -74,6 +72,7 @@ class _python_symbols(Symbols):
     for_stmt: int
     funcdef: int
     global_stmt: int
+    guard: int
     if_stmt: int
     import_as_name: int
     import_as_names: int
@@ -82,6 +81,7 @@ class _python_symbols(Symbols):
     import_stmt: int
     lambdef: int
     listmaker: int
+    match_stmt: int
     namedexpr_test: int
     not_test: int
     old_comp_for: int
@@ -91,9 +91,11 @@ class _python_symbols(Symbols):
     old_test: int
     or_test: int
     parameters: int
+    paramspec: int
     pass_stmt: int
+    pattern: int
+    patterns: int
     power: int
-    print_stmt: int
     raise_stmt: int
     return_stmt: int
     shift_expr: int
@@ -101,6 +103,7 @@ class _python_symbols(Symbols):
     single_input: int
     sliceop: int
     small_stmt: int
+    subject_expr: int
     star_expr: int
     stmt: int
     subscript: int
@@ -116,17 +119,21 @@ class _python_symbols(Symbols):
     tfpdef: int
     tfplist: int
     tname: int
+    tname_star: int
     trailer: int
     try_stmt: int
+    type_stmt: int
     typedargslist: int
+    typeparam: int
+    typeparams: int
+    typevar: int
+    typevartuple: int
     varargslist: int
     vfpdef: int
     vfplist: int
     vname: int
     while_stmt: int
-    with_item: int
     with_stmt: int
-    with_var: int
     xor_expr: int
     yield_arg: int
     yield_expr: int
@@ -144,21 +151,17 @@ class _pattern_symbols(Symbols):
 
 
 python_grammar: Grammar
-python_grammar_no_print_statement: Grammar
-python_grammar_no_print_statement_no_exec_statement: Grammar
-python_grammar_no_print_statement_no_exec_statement_async_keywords: Grammar
-python_grammar_no_exec_statement: Grammar
+python_grammar_async_keywords: Grammar
+python_grammar_soft_keywords: Grammar
 pattern_grammar: Grammar
-
 python_symbols: _python_symbols
 pattern_symbols: _pattern_symbols
 
 
 def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None:
     global python_grammar
-    global python_grammar_no_print_statement
-    global python_grammar_no_print_statement_no_exec_statement
-    global python_grammar_no_print_statement_no_exec_statement_async_keywords
+    global python_grammar_async_keywords
+    global python_grammar_soft_keywords
     global python_symbols
     global pattern_grammar
     global pattern_symbols
@@ -169,27 +172,27 @@ def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None:
         os.path.dirname(__file__), "PatternGrammar.txt"
     )
 
-    # Python 2
     python_grammar = driver.load_packaged_grammar("blib2to3", _GRAMMAR_FILE, cache_dir)
+    assert "print" not in python_grammar.keywords
+    assert "exec" not in python_grammar.keywords
 
-    python_symbols = _python_symbols(python_grammar)
+    soft_keywords = python_grammar.soft_keywords.copy()
+    python_grammar.soft_keywords.clear()
 
-    # Python 2 + from __future__ import print_function
-    python_grammar_no_print_statement = python_grammar.copy()
-    del python_grammar_no_print_statement.keywords["print"]
+    python_symbols = _python_symbols(python_grammar)
 
     # Python 3.0-3.6
-    python_grammar_no_print_statement_no_exec_statement = python_grammar.copy()
-    del python_grammar_no_print_statement_no_exec_statement.keywords["print"]
-    del python_grammar_no_print_statement_no_exec_statement.keywords["exec"]
+    python_grammar.version = (3, 0)
 
     # Python 3.7+
-    python_grammar_no_print_statement_no_exec_statement_async_keywords = (
-        python_grammar_no_print_statement_no_exec_statement.copy()
-    )
-    python_grammar_no_print_statement_no_exec_statement_async_keywords.async_keywords = (
-        True
-    )
+    python_grammar_async_keywords = python_grammar.copy()
+    python_grammar_async_keywords.async_keywords = True
+    python_grammar_async_keywords.version = (3, 7)
+
+    # Python 3.10+
+    python_grammar_soft_keywords = python_grammar_async_keywords.copy()
+    python_grammar_soft_keywords.soft_keywords = soft_keywords
+    python_grammar_soft_keywords.version = (3, 10)
 
     pattern_grammar = driver.load_packaged_grammar(
         "blib2to3", _PATTERN_GRAMMAR_FILE, cache_dir
index 7843467e0129d2c3f281ffab3ff76da7d53001f3..2a0cd6d196a3410536b4e9a07d5ded47d6c90b54 100644 (file)
@@ -10,22 +10,21 @@ even the comments and whitespace between tokens.
 There's also a pattern matching implementation here.
 """
 
-# mypy: allow-untyped-defs
+# mypy: allow-untyped-defs, allow-incomplete-defs
 
 from typing import (
     Any,
-    Callable,
     Dict,
+    Iterable,
     Iterator,
     List,
     Optional,
-    Text,
+    Set,
     Tuple,
     TypeVar,
     Union,
-    Set,
-    Iterable,
 )
+
 from blib2to3.pgen2.grammar import Grammar
 
 __author__ = "Guido van Rossum <guido@python.org>"
@@ -35,10 +34,10 @@ from io import StringIO
 
 HUGE: int = 0x7FFFFFFF  # maximum repeat count, default max
 
-_type_reprs: Dict[int, Union[Text, int]] = {}
+_type_reprs: Dict[int, Union[str, int]] = {}
 
 
-def type_repr(type_num: int) -> Union[Text, int]:
+def type_repr(type_num: int) -> Union[str, int]:
     global _type_reprs
     if not _type_reprs:
         from .pygram import python_symbols
@@ -52,15 +51,14 @@ def type_repr(type_num: int) -> Union[Text, int]:
     return _type_reprs.setdefault(type_num, type_num)
 
 
-_P = TypeVar("_P")
+_P = TypeVar("_P", bound="Base")
 
 NL = Union["Node", "Leaf"]
-Context = Tuple[Text, Tuple[int, int]]
-RawNode = Tuple[int, Optional[Text], Optional[Context], Optional[List[NL]]]
+Context = Tuple[str, Tuple[int, int]]
+RawNode = Tuple[int, Optional[str], Optional[Context], Optional[List[NL]]]
 
 
-class Base(object):
-
+class Base:
     """
     Abstract base class for Node and Leaf.
 
@@ -92,10 +90,8 @@ class Base(object):
             return NotImplemented
         return self._eq(other)
 
-    __hash__ = None  # type: Any  # For Py3 compatibility.
-
     @property
-    def prefix(self) -> Text:
+    def prefix(self) -> str:
         raise NotImplementedError
 
     def _eq(self: _P, other: _P) -> bool:
@@ -109,6 +105,9 @@ class Base(object):
         """
         raise NotImplementedError
 
+    def __deepcopy__(self: _P, memo: Any) -> _P:
+        return self.clone()
+
     def clone(self: _P) -> _P:
         """
         Return a cloned (deep) copy of self.
@@ -225,7 +224,7 @@ class Base(object):
             return 0
         return 1 + self.parent.depth()
 
-    def get_suffix(self) -> Text:
+    def get_suffix(self) -> str:
         """
         Return the string immediately following the invocant node. This is
         effectively equivalent to node.next_sibling.prefix
@@ -238,18 +237,17 @@ class Base(object):
 
 
 class Node(Base):
-
     """Concrete implementation for interior nodes."""
 
     fixers_applied: Optional[List[Any]]
-    used_names: Optional[Set[Text]]
+    used_names: Optional[Set[str]]
 
     def __init__(
         self,
         type: int,
         children: List[NL],
         context: Optional[Any] = None,
-        prefix: Optional[Text] = None,
+        prefix: Optional[str] = None,
         fixers_applied: Optional[List[Any]] = None,
     ) -> None:
         """
@@ -274,16 +272,16 @@ class Node(Base):
         else:
             self.fixers_applied = None
 
-    def __repr__(self) -> Text:
+    def __repr__(self) -> str:
         """Return a canonical string representation."""
         assert self.type is not None
-        return "%s(%s, %r)" % (
+        return "{}({}, {!r})".format(
             self.__class__.__name__,
             type_repr(self.type),
             self.children,
         )
 
-    def __str__(self) -> Text:
+    def __str__(self) -> str:
         """
         Return a pretty string representation.
 
@@ -291,7 +289,7 @@ class Node(Base):
         """
         return "".join(map(str, self.children))
 
-    def _eq(self, other) -> bool:
+    def _eq(self, other: Base) -> bool:
         """Compare two nodes for equality."""
         return (self.type, self.children) == (other.type, other.children)
 
@@ -317,7 +315,7 @@ class Node(Base):
             yield from child.pre_order()
 
     @property
-    def prefix(self) -> Text:
+    def prefix(self) -> str:
         """
         The whitespace and comments preceding this node in the input.
         """
@@ -326,7 +324,7 @@ class Node(Base):
         return self.children[0].prefix
 
     @prefix.setter
-    def prefix(self, prefix) -> None:
+    def prefix(self, prefix: str) -> None:
         if self.children:
             self.children[0].prefix = prefix
 
@@ -379,26 +377,32 @@ class Node(Base):
 
 
 class Leaf(Base):
-
     """Concrete implementation for leaf nodes."""
 
     # Default values for instance variables
-    value: Text
+    value: str
     fixers_applied: List[Any]
     bracket_depth: int
-    opening_bracket: "Leaf"
-    used_names: Optional[Set[Text]]
+    # Changed later in brackets.py
+    opening_bracket: Optional["Leaf"] = None
+    used_names: Optional[Set[str]]
     _prefix = ""  # Whitespace and comments preceding this token in the input
     lineno: int = 0  # Line where this token starts in the input
     column: int = 0  # Column where this token starts in the input
+    # If not None, this Leaf is created by converting a block of fmt off/skip
+    # code, and `fmt_pass_converted_first_leaf` points to the first Leaf in the
+    # converted code.
+    fmt_pass_converted_first_leaf: Optional["Leaf"] = None
 
     def __init__(
         self,
         type: int,
-        value: Text,
+        value: str,
         context: Optional[Context] = None,
-        prefix: Optional[Text] = None,
+        prefix: Optional[str] = None,
         fixers_applied: List[Any] = [],
+        opening_bracket: Optional["Leaf"] = None,
+        fmt_pass_converted_first_leaf: Optional["Leaf"] = None,
     ) -> None:
         """
         Initializer.
@@ -416,27 +420,29 @@ class Leaf(Base):
             self._prefix = prefix
         self.fixers_applied: Optional[List[Any]] = fixers_applied[:]
         self.children = []
+        self.opening_bracket = opening_bracket
+        self.fmt_pass_converted_first_leaf = fmt_pass_converted_first_leaf
 
     def __repr__(self) -> str:
         """Return a canonical string representation."""
         from .pgen2.token import tok_name
 
         assert self.type is not None
-        return "%s(%s, %r)" % (
+        return "{}({}, {!r})".format(
             self.__class__.__name__,
             tok_name.get(self.type, self.type),
             self.value,
         )
 
-    def __str__(self) -> Text:
+    def __str__(self) -> str:
         """
         Return a pretty string representation.
 
         This reproduces the input source exactly.
         """
-        return self.prefix + str(self.value)
+        return self._prefix + str(self.value)
 
-    def _eq(self, other) -> bool:
+    def _eq(self, other: "Leaf") -> bool:
         """Compare two nodes for equality."""
         return (self.type, self.value) == (other.type, other.value)
 
@@ -462,14 +468,14 @@ class Leaf(Base):
         yield self
 
     @property
-    def prefix(self) -> Text:
+    def prefix(self) -> str:
         """
         The whitespace and comments preceding this token in the input.
         """
         return self._prefix
 
     @prefix.setter
-    def prefix(self, prefix) -> None:
+    def prefix(self, prefix: str) -> None:
         self.changed()
         self._prefix = prefix
 
@@ -494,11 +500,10 @@ def convert(gr: Grammar, raw_node: RawNode) -> NL:
         return Leaf(type, value or "", context=context)
 
 
-_Results = Dict[Text, NL]
-
+_Results = Dict[str, NL]
 
-class BasePattern(object):
 
+class BasePattern:
     """
     A pattern is a tree matching pattern.
 
@@ -517,19 +522,19 @@ class BasePattern(object):
     type: Optional[int]
     type = None  # Node type (token if < 256, symbol if >= 256)
     content: Any = None  # Optional content matching pattern
-    name: Optional[Text] = None  # Optional name used to store match in results dict
+    name: Optional[str] = None  # Optional name used to store match in results dict
 
     def __new__(cls, *args, **kwds):
         """Constructor that prevents BasePattern from being instantiated."""
         assert cls is not BasePattern, "Cannot instantiate BasePattern"
         return object.__new__(cls)
 
-    def __repr__(self) -> Text:
+    def __repr__(self) -> str:
         assert self.type is not None
         args = [type_repr(self.type), self.content, self.name]
         while args and args[-1] is None:
             del args[-1]
-        return "%s(%s)" % (self.__class__.__name__, ", ".join(map(repr, args)))
+        return "{}({})".format(self.__class__.__name__, ", ".join(map(repr, args)))
 
     def _submatch(self, node, results=None) -> bool:
         raise NotImplementedError
@@ -593,8 +598,8 @@ class LeafPattern(BasePattern):
     def __init__(
         self,
         type: Optional[int] = None,
-        content: Optional[Text] = None,
-        name: Optional[Text] = None,
+        content: Optional[str] = None,
+        name: Optional[str] = None,
     ) -> None:
         """
         Initializer.  Takes optional type, content, and name.
@@ -615,7 +620,7 @@ class LeafPattern(BasePattern):
         self.content = content
         self.name = name
 
-    def match(self, node: NL, results=None):
+    def match(self, node: NL, results=None) -> bool:
         """Override match() to insist on a leaf node."""
         if not isinstance(node, Leaf):
             return False
@@ -638,14 +643,13 @@ class LeafPattern(BasePattern):
 
 
 class NodePattern(BasePattern):
-
     wildcards: bool = False
 
     def __init__(
         self,
         type: Optional[int] = None,
-        content: Optional[Iterable[Text]] = None,
-        name: Optional[Text] = None,
+        content: Optional[Iterable[str]] = None,
+        name: Optional[str] = None,
     ) -> None:
         """
         Initializer.  Takes optional type, content, and name.
@@ -669,10 +673,13 @@ class NodePattern(BasePattern):
             newcontent = list(content)
             for i, item in enumerate(newcontent):
                 assert isinstance(item, BasePattern), (i, item)
-                if isinstance(item, WildcardPattern):
-                    self.wildcards = True
+                # I don't even think this code is used anywhere, but it does cause
+                # unreachable errors from mypy. This function's signature does look
+                # odd though *shrug*.
+                if isinstance(item, WildcardPattern):  # type: ignore[unreachable]
+                    self.wildcards = True  # type: ignore[unreachable]
         self.type = type
-        self.content = newcontent
+        self.content = newcontent  # TODO: this is unbound when content is None
         self.name = name
 
     def _submatch(self, node, results=None) -> bool:
@@ -704,7 +711,6 @@ class NodePattern(BasePattern):
 
 
 class WildcardPattern(BasePattern):
-
     """
     A wildcard pattern can match zero or more nodes.
 
@@ -722,10 +728,10 @@ class WildcardPattern(BasePattern):
 
     def __init__(
         self,
-        content: Optional[Text] = None,
+        content: Optional[str] = None,
         min: int = 0,
         max: int = HUGE,
-        name: Optional[Text] = None,
+        name: Optional[str] = None,
     ) -> None:
         """
         Initializer.
@@ -914,7 +920,7 @@ class WildcardPattern(BasePattern):
 
 
 class NegatedPattern(BasePattern):
-    def __init__(self, content: Optional[Any] = None) -> None:
+    def __init__(self, content: Optional[BasePattern] = None) -> None:
         """
         Initializer.
 
@@ -935,7 +941,7 @@ class NegatedPattern(BasePattern):
         # We only match an empty sequence of nodes in its entirety
         return len(nodes) == 0
 
-    def generate_matches(self, nodes) -> Iterator[Tuple[int, _Results]]:
+    def generate_matches(self, nodes: List[NL]) -> Iterator[Tuple[int, _Results]]:
         if self.content is None:
             # Return a match if there is an empty sequence
             if len(nodes) == 0:
@@ -975,6 +981,3 @@ def generate_matches(
                     r.update(r0)
                     r.update(r1)
                     yield c0 + c1, r
-
-
-_Convert = Callable[[Grammar, RawNode], Any]
index 5bc494d599966e2631f37c79d180e260c7f746be..a3d262bc53d9bd1b3463cc76e13858b2c7dc3616 100644 (file)
@@ -1,6 +1,6 @@
 coverage >= 5.3
 pre-commit
 pytest >= 6.1.1
-pytest-xdist >= 2.2.1
-pytest-cov >= 2.11.1
+pytest-xdist >= 3.0.2
+pytest-cov >= 4.1.0
 tox
index 67517268d1bd3e9acad617318127998fbe603d1a..1a0dd747d8ecc3e422cf47a150943b60ad24d692 100644 (file)
@@ -1 +1,28 @@
+import pytest
+
 pytest_plugins = ["tests.optional"]
+
+PRINT_FULL_TREE: bool = False
+PRINT_TREE_DIFF: bool = True
+
+
+def pytest_addoption(parser: pytest.Parser) -> None:
+    parser.addoption(
+        "--print-full-tree",
+        action="store_true",
+        default=False,
+        help="print full syntax trees on failed tests",
+    )
+    parser.addoption(
+        "--print-tree-diff",
+        action="store_true",
+        default=True,
+        help="print diff of syntax trees on failed tests",
+    )
+
+
+def pytest_configure(config: pytest.Config) -> None:
+    global PRINT_FULL_TREE
+    global PRINT_TREE_DIFF
+    PRINT_FULL_TREE = config.getoption("--print-full-tree")
+    PRINT_TREE_DIFF = config.getoption("--print-tree-diff")
diff --git a/.vim/bundle/black/tests/data/cases/attribute_access_on_number_literals.py b/.vim/bundle/black/tests/data/cases/attribute_access_on_number_literals.py
new file mode 100644 (file)
index 0000000..7c16bdf
--- /dev/null
@@ -0,0 +1,47 @@
+x = 123456789 .bit_count()
+x = (123456).__abs__()
+x = .1.is_integer()
+x = 1. .imag
+x = 1E+1.imag
+x = 1E-1.real
+x = 123456789.123456789.hex()
+x = 123456789.123456789E123456789 .real
+x = 123456789E123456789 .conjugate()
+x = 123456789J.real
+x = 123456789.123456789J.__add__(0b1011.bit_length())
+x = 0XB1ACC.conjugate()
+x = 0B1011 .conjugate()
+x = 0O777 .real
+x = 0.000000006  .hex()
+x = -100.0000J
+
+if 10 .real:
+    ...
+
+y = 100[no]
+y = 100(no)
+
+# output
+
+x = (123456789).bit_count()
+x = (123456).__abs__()
+x = (0.1).is_integer()
+x = (1.0).imag
+x = (1e1).imag
+x = (1e-1).real
+x = (123456789.123456789).hex()
+x = (123456789.123456789e123456789).real
+x = (123456789e123456789).conjugate()
+x = 123456789j.real
+x = 123456789.123456789j.__add__(0b1011.bit_length())
+x = 0xB1ACC.conjugate()
+x = 0b1011.conjugate()
+x = 0o777.real
+x = (0.000000006).hex()
+x = -100.0000j
+
+if (10).real:
+    ...
+
+y = 100[no]
+y = 100(no)
similarity index 98%
rename from .vim/bundle/black/tests/data/comments2.py
rename to .vim/bundle/black/tests/data/cases/comments2.py
index 4eea013151a1f205fd4ae074b35293dc686014ae..1487dc4b6e2c546a8952fef91315cb57b063e29d 100644 (file)
@@ -154,6 +154,9 @@ class Test:
                 not parsed.hostname.strip()):
             pass
 
+
+a = "type comment with trailing space"  # type: str   
+
 #######################
 ### SECTION COMMENT ###
 #######################
@@ -226,6 +229,7 @@ else:
     add_compiler(compilers[(7.0, 32)])
     # add_compiler(compilers[(7.1, 64)])
 
+
 # Comment before function.
 def inline_comments_in_brackets_ruin_everything():
     if typedargslist:
@@ -331,6 +335,8 @@ class Test:
             pass
 
 
+a = "type comment with trailing space"  # type: str
+
 #######################
 ### SECTION COMMENT ###
 #######################
similarity index 99%
rename from .vim/bundle/black/tests/data/comments3.py
rename to .vim/bundle/black/tests/data/cases/comments3.py
index fbbef6dcc6bdcf7657d59284b3b9c335116b1670..f964bee66517c592afb03dbdeb317b3b40dd8552 100644 (file)
@@ -1,6 +1,7 @@
 # The percent-percent comments are Spyder IDE cells.
 
-#%%
+
+# %%
 def func():
     x = """
     a really long string
@@ -44,4 +45,4 @@ def func():
     )
 
 
-#%%
\ No newline at end of file
+# %%
\ No newline at end of file
similarity index 98%
rename from .vim/bundle/black/tests/data/comments4.py
rename to .vim/bundle/black/tests/data/cases/comments4.py
index 2147d41c9da74bcd6df291509911740f95393319..9f4f39d83599dde87d3f720ea3b4cbbc3a33988c 100644 (file)
@@ -85,7 +85,7 @@ def foo2(list_a, list_b):
 
 def foo3(list_a, list_b):
     return (
-        # Standlone comment but weirdly placed.
+        # Standalone comment but weirdly placed.
         User.query.filter(User.foo == "bar")
         .filter(
             db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
similarity index 86%
rename from .vim/bundle/black/tests/data/comments5.py
rename to .vim/bundle/black/tests/data/cases/comments5.py
index d83b6b8ff47cc6f731aee0f7a3369f3d6e7db960..bda40619f62ce28ea7259d78a53403acdc237359 100644 (file)
@@ -58,10 +58,12 @@ def decorated1():
     ...
 
 
-# Note: crappy but inevitable.  The current design of EmptyLineTracker doesn't
-# allow this to work correctly.  The user will have to split those lines by
-# hand.
+# Note: this is fixed in
+# Preview.empty_lines_before_class_or_def_with_leading_comments.
+# In the current style, the user will have to split those lines by hand.
 some_instruction
+
+
 # This comment should be split from `some_instruction` by two lines but isn't.
 def g():
     ...
diff --git a/.vim/bundle/black/tests/data/cases/comments8.py b/.vim/bundle/black/tests/data/cases/comments8.py
new file mode 100644 (file)
index 0000000..a2030c2
--- /dev/null
@@ -0,0 +1,15 @@
+# The percent-percent comments are Spyder IDE cells.
+# Both `#%%`` and `# %%` are accepted, so `black` standardises
+# to the latter.
+
+#%%
+# %%
+
+# output
+
+# The percent-percent comments are Spyder IDE cells.
+# Both `#%%`` and `# %%` are accepted, so `black` standardises
+# to the latter.
+
+# %%
+# %%
diff --git a/.vim/bundle/black/tests/data/cases/comments9.py b/.vim/bundle/black/tests/data/cases/comments9.py
new file mode 100644 (file)
index 0000000..77b2555
--- /dev/null
@@ -0,0 +1,305 @@
+# Test for https://github.com/psf/black/issues/246.
+
+some = statement
+# This comment should be split from the statement above by two lines.
+def function():
+    pass
+
+
+some = statement
+# This multiline comments section
+# should be split from the statement
+# above by two lines.
+def function():
+    pass
+
+
+some = statement
+# This comment should be split from the statement above by two lines.
+async def async_function():
+    pass
+
+
+some = statement
+# This comment should be split from the statement above by two lines.
+class MyClass:
+    pass
+
+
+some = statement
+# This should be stick to the statement above
+
+# This should be split from the above by two lines
+class MyClassWithComplexLeadingComments:
+    pass
+
+
+class ClassWithDocstring:
+    """A docstring."""
+# Leading comment after a class with just a docstring
+class MyClassAfterAnotherClassWithDocstring:
+    pass
+
+
+some = statement
+# leading 1
+@deco1
+# leading 2
+# leading 2 extra
+@deco2(with_args=True)
+# leading 3
+@deco3
+# leading 4
+def decorated():
+    pass
+
+
+some = statement
+# leading 1
+@deco1
+# leading 2
+@deco2(with_args=True)
+
+# leading 3 that already has an empty line
+@deco3
+# leading 4
+def decorated_with_split_leading_comments():
+    pass
+
+
+some = statement
+# leading 1
+@deco1
+# leading 2
+@deco2(with_args=True)
+# leading 3
+@deco3
+
+# leading 4 that already has an empty line
+def decorated_with_split_leading_comments():
+    pass
+
+
+def main():
+    if a:
+        # Leading comment before inline function
+        def inline():
+            pass
+        # Another leading comment
+        def another_inline():
+            pass
+    else:
+        # More leading comments
+        def inline_after_else():
+            pass
+
+
+if a:
+    # Leading comment before "top-level inline" function
+    def top_level_quote_inline():
+        pass
+    # Another leading comment
+    def another_top_level_quote_inline_inline():
+        pass
+else:
+    # More leading comments
+    def top_level_quote_inline_after_else():
+        pass
+
+
+class MyClass:
+    # First method has no empty lines between bare class def.
+    # More comments.
+    def first_method(self):
+        pass
+
+
+# Regression test for https://github.com/psf/black/issues/3454.
+def foo():
+    pass
+    # Trailing comment that belongs to this function
+
+
+@decorator1
+@decorator2  # fmt: skip
+def bar():
+    pass
+
+
+# Regression test for https://github.com/psf/black/issues/3454.
+def foo():
+    pass
+    # Trailing comment that belongs to this function.
+    # NOTE this comment only has one empty line below, and the formatter
+    # should enforce two blank lines.
+
+@decorator1
+# A standalone comment
+def bar():
+    pass
+
+
+# output
+
+
+# Test for https://github.com/psf/black/issues/246.
+
+some = statement
+
+
+# This comment should be split from the statement above by two lines.
+def function():
+    pass
+
+
+some = statement
+
+
+# This multiline comments section
+# should be split from the statement
+# above by two lines.
+def function():
+    pass
+
+
+some = statement
+
+
+# This comment should be split from the statement above by two lines.
+async def async_function():
+    pass
+
+
+some = statement
+
+
+# This comment should be split from the statement above by two lines.
+class MyClass:
+    pass
+
+
+some = statement
+# This should be stick to the statement above
+
+
+# This should be split from the above by two lines
+class MyClassWithComplexLeadingComments:
+    pass
+
+
+class ClassWithDocstring:
+    """A docstring."""
+
+
+# Leading comment after a class with just a docstring
+class MyClassAfterAnotherClassWithDocstring:
+    pass
+
+
+some = statement
+
+
+# leading 1
+@deco1
+# leading 2
+# leading 2 extra
+@deco2(with_args=True)
+# leading 3
+@deco3
+# leading 4
+def decorated():
+    pass
+
+
+some = statement
+
+
+# leading 1
+@deco1
+# leading 2
+@deco2(with_args=True)
+
+# leading 3 that already has an empty line
+@deco3
+# leading 4
+def decorated_with_split_leading_comments():
+    pass
+
+
+some = statement
+
+
+# leading 1
+@deco1
+# leading 2
+@deco2(with_args=True)
+# leading 3
+@deco3
+
+# leading 4 that already has an empty line
+def decorated_with_split_leading_comments():
+    pass
+
+
+def main():
+    if a:
+        # Leading comment before inline function
+        def inline():
+            pass
+
+        # Another leading comment
+        def another_inline():
+            pass
+
+    else:
+        # More leading comments
+        def inline_after_else():
+            pass
+
+
+if a:
+    # Leading comment before "top-level inline" function
+    def top_level_quote_inline():
+        pass
+
+    # Another leading comment
+    def another_top_level_quote_inline_inline():
+        pass
+
+else:
+    # More leading comments
+    def top_level_quote_inline_after_else():
+        pass
+
+
+class MyClass:
+    # First method has no empty lines between bare class def.
+    # More comments.
+    def first_method(self):
+        pass
+
+
+# Regression test for https://github.com/psf/black/issues/3454.
+def foo():
+    pass
+    # Trailing comment that belongs to this function
+
+
+@decorator1
+@decorator2  # fmt: skip
+def bar():
+    pass
+
+
+# Regression test for https://github.com/psf/black/issues/3454.
+def foo():
+    pass
+    # Trailing comment that belongs to this function.
+    # NOTE this comment only has one empty line below, and the formatter
+    # should enforce two blank lines.
+
+
+@decorator1
+# A standalone comment
+def bar():
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/conditional_expression.py b/.vim/bundle/black/tests/data/cases/conditional_expression.py
new file mode 100644 (file)
index 0000000..c30cd76
--- /dev/null
@@ -0,0 +1,161 @@
+# flags: --preview
+long_kwargs_single_line = my_function(
+    foo="test, this is a sample value",
+    bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz,
+    baz="hello, this is a another value",
+)
+
+multiline_kwargs_indented = my_function(
+    foo="test, this is a sample value",
+    bar=some_long_value_name_foo_bar_baz
+    if some_boolean_variable
+    else some_fallback_value_foo_bar_baz,
+    baz="hello, this is a another value",
+)
+
+imploding_kwargs = my_function(
+    foo="test, this is a sample value",
+    bar=a
+    if foo
+    else b,
+    baz="hello, this is a another value",
+)
+
+imploding_line = (
+    1
+    if 1 + 1 == 2
+    else 0
+)
+
+exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter"
+
+positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz)
+
+def weird_default_argument(x=some_long_value_name_foo_bar_baz
+        if SOME_CONSTANT
+        else some_fallback_value_foo_bar_baz):
+    pass
+
+nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if
+                                                    nesting_test_expressions else some_fallback_value_foo_bar_baz) \
+    else "this one is a little shorter"
+
+generator_expression = (
+    some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable
+)
+
+
+def limit_offset_sql(self, low_mark, high_mark):
+    """Return LIMIT/OFFSET SQL clause."""
+    limit, offset = self._get_limit_offset_params(low_mark, high_mark)
+    return " ".join(
+        sql
+        for sql in (
+            "LIMIT %d" % limit if limit else None,
+            ("OFFSET %d" % offset) if offset else None,
+        )
+        if sql
+    )
+
+
+def something():
+    clone._iterable_class = (
+        NamedValuesListIterable
+        if named
+        else FlatValuesListIterable
+        if flat
+        else ValuesListIterable
+    )
+
+# output
+
+long_kwargs_single_line = my_function(
+    foo="test, this is a sample value",
+    bar=(
+        some_long_value_name_foo_bar_baz
+        if some_boolean_variable
+        else some_fallback_value_foo_bar_baz
+    ),
+    baz="hello, this is a another value",
+)
+
+multiline_kwargs_indented = my_function(
+    foo="test, this is a sample value",
+    bar=(
+        some_long_value_name_foo_bar_baz
+        if some_boolean_variable
+        else some_fallback_value_foo_bar_baz
+    ),
+    baz="hello, this is a another value",
+)
+
+imploding_kwargs = my_function(
+    foo="test, this is a sample value",
+    bar=a if foo else b,
+    baz="hello, this is a another value",
+)
+
+imploding_line = 1 if 1 + 1 == 2 else 0
+
+exploding_line = (
+    "hello this is a slightly long string"
+    if some_long_value_name_foo_bar_baz
+    else "this one is a little shorter"
+)
+
+positional_argument_test(
+    some_long_value_name_foo_bar_baz
+    if some_boolean_variable
+    else some_fallback_value_foo_bar_baz
+)
+
+
+def weird_default_argument(
+    x=(
+        some_long_value_name_foo_bar_baz
+        if SOME_CONSTANT
+        else some_fallback_value_foo_bar_baz
+    ),
+):
+    pass
+
+
+nested = (
+    "hello this is a slightly long string"
+    if (
+        some_long_value_name_foo_bar_baz
+        if nesting_test_expressions
+        else some_fallback_value_foo_bar_baz
+    )
+    else "this one is a little shorter"
+)
+
+generator_expression = (
+    (
+        some_long_value_name_foo_bar_baz
+        if some_boolean_variable
+        else some_fallback_value_foo_bar_baz
+    )
+    for some_boolean_variable in some_iterable
+)
+
+
+def limit_offset_sql(self, low_mark, high_mark):
+    """Return LIMIT/OFFSET SQL clause."""
+    limit, offset = self._get_limit_offset_params(low_mark, high_mark)
+    return " ".join(
+        sql
+        for sql in (
+            "LIMIT %d" % limit if limit else None,
+            ("OFFSET %d" % offset) if offset else None,
+        )
+        if sql
+    )
+
+
+def something():
+    clone._iterable_class = (
+        NamedValuesListIterable
+        if named
+        else FlatValuesListIterable if flat else ValuesListIterable
+    )
similarity index 72%
rename from .vim/bundle/black/tests/data/docstring.py
rename to .vim/bundle/black/tests/data/cases/docstring.py
index 96bcf525b16fe56538739f4c895e021a4766dadd..c31d6a68783e3d904186c292c3e933986f471c8d 100644 (file)
@@ -173,6 +173,11 @@ def multiline_backslash_2():
   '''
   hey there \ '''
 
+# Regression test for #3425
+def multiline_backslash_really_long_dont_crash():
+    """
+    hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """
+
 
 def multiline_backslash_3():
   '''
@@ -188,6 +193,34 @@ def my_god_its_full_of_stars_2():
     "I'm sorry Dave "
 
 
+def docstring_almost_at_line_limit():
+    """long docstring................................................................."""
+
+
+def docstring_almost_at_line_limit2():
+    """long docstring.................................................................
+
+    ..................................................................................
+    """
+
+
+def docstring_at_line_limit():
+    """long docstring................................................................"""
+
+
+def multiline_docstring_at_line_limit():
+    """first line-----------------------------------------------------------------------
+
+    second line----------------------------------------------------------------------"""
+
+
+def stable_quote_normalization_with_immediate_inner_single_quote(self):
+    ''''<text here>
+
+    <text here, since without another non-empty line black is stable>
+    '''
+
+
 # output
 
 class MyClass:
@@ -363,6 +396,12 @@ def multiline_backslash_2():
     hey there \ """
 
 
+# Regression test for #3425
+def multiline_backslash_really_long_dont_crash():
+    """
+    hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """
+
+
 def multiline_backslash_3():
     """
     already escaped \\"""
@@ -375,3 +414,31 @@ def my_god_its_full_of_stars_1():
 # the space below is actually a \u2001, removed in output
 def my_god_its_full_of_stars_2():
     "I'm sorry Dave"
+
+
+def docstring_almost_at_line_limit():
+    """long docstring................................................................."""
+
+
+def docstring_almost_at_line_limit2():
+    """long docstring.................................................................
+
+    ..................................................................................
+    """
+
+
+def docstring_at_line_limit():
+    """long docstring................................................................"""
+
+
+def multiline_docstring_at_line_limit():
+    """first line-----------------------------------------------------------------------
+
+    second line----------------------------------------------------------------------"""
+
+
+def stable_quote_normalization_with_immediate_inner_single_quote(self):
+    """'<text here>
+
+    <text here, since without another non-empty line black is stable>
+    """
diff --git a/.vim/bundle/black/tests/data/cases/docstring_no_extra_empty_line_before_eof.py b/.vim/bundle/black/tests/data/cases/docstring_no_extra_empty_line_before_eof.py
new file mode 100644 (file)
index 0000000..6fea860
--- /dev/null
@@ -0,0 +1,4 @@
+# Make sure when the file ends with class's docstring,
+# It doesn't add extra blank lines.
+class ClassWithDocstring:
+    """A docstring."""
similarity index 98%
rename from .vim/bundle/black/tests/data/docstring_no_string_normalization.py
rename to .vim/bundle/black/tests/data/cases/docstring_no_string_normalization.py
index a90b578f09afb71ddff8e84e8ede8dcc5372fe1d..4ec6b8a01535a17d092a4ac0d255c43d8ec524d2 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --skip-string-normalization
 class ALonelyClass:
     '''
     A multiline class docstring.
diff --git a/.vim/bundle/black/tests/data/cases/docstring_preview.py b/.vim/bundle/black/tests/data/cases/docstring_preview.py
new file mode 100644 (file)
index 0000000..ff4819a
--- /dev/null
@@ -0,0 +1,103 @@
+def docstring_almost_at_line_limit():
+    """long docstring.................................................................
+    """
+
+
+def docstring_almost_at_line_limit_with_prefix():
+    f"""long docstring................................................................
+    """
+
+
+def mulitline_docstring_almost_at_line_limit():
+    """long docstring.................................................................
+
+    ..................................................................................
+    """
+
+
+def mulitline_docstring_almost_at_line_limit_with_prefix():
+    f"""long docstring................................................................
+
+    ..................................................................................
+    """
+
+
+def docstring_at_line_limit():
+    """long docstring................................................................"""
+
+
+def docstring_at_line_limit_with_prefix():
+    f"""long docstring..............................................................."""
+
+
+def multiline_docstring_at_line_limit():
+    """first line-----------------------------------------------------------------------
+
+    second line----------------------------------------------------------------------"""
+
+
+def multiline_docstring_at_line_limit_with_prefix():
+    f"""first line----------------------------------------------------------------------
+
+    second line----------------------------------------------------------------------"""
+
+
+def single_quote_docstring_over_line_limit():
+    "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)."
+
+
+def single_quote_docstring_over_line_limit2():
+    'We do not want to put the closing quote on a new line as that is invalid (see GH-3141).'
+
+
+# output
+
+
+def docstring_almost_at_line_limit():
+    """long docstring................................................................."""
+
+
+def docstring_almost_at_line_limit_with_prefix():
+    f"""long docstring................................................................"""
+
+
+def mulitline_docstring_almost_at_line_limit():
+    """long docstring.................................................................
+
+    ..................................................................................
+    """
+
+
+def mulitline_docstring_almost_at_line_limit_with_prefix():
+    f"""long docstring................................................................
+
+    ..................................................................................
+    """
+
+
+def docstring_at_line_limit():
+    """long docstring................................................................"""
+
+
+def docstring_at_line_limit_with_prefix():
+    f"""long docstring..............................................................."""
+
+
+def multiline_docstring_at_line_limit():
+    """first line-----------------------------------------------------------------------
+
+    second line----------------------------------------------------------------------"""
+
+
+def multiline_docstring_at_line_limit_with_prefix():
+    f"""first line----------------------------------------------------------------------
+
+    second line----------------------------------------------------------------------"""
+
+
+def single_quote_docstring_over_line_limit():
+    "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)."
+
+
+def single_quote_docstring_over_line_limit2():
+    "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)."
diff --git a/.vim/bundle/black/tests/data/cases/docstring_preview_no_string_normalization.py b/.vim/bundle/black/tests/data/cases/docstring_preview_no_string_normalization.py
new file mode 100644 (file)
index 0000000..712c736
--- /dev/null
@@ -0,0 +1,11 @@
+# flags: --preview --skip-string-normalization
+def do_not_touch_this_prefix():
+    R"""There was a bug where docstring prefixes would be normalized even with -S."""
+
+
+def do_not_touch_this_prefix2():
+    FR'There was a bug where docstring prefixes would be normalized even with -S.'
+
+
+def do_not_touch_this_prefix3():
+    u'''There was a bug where docstring prefixes would be normalized even with -S.'''
similarity index 99%
rename from .vim/bundle/black/tests/data/empty_lines.py
rename to .vim/bundle/black/tests/data/cases/empty_lines.py
index 4c03e432383c174127c803dea8076488a09a8daf..4fd47b93dcacaec67765c8129171f8f07a8523f8 100644 (file)
@@ -119,7 +119,6 @@ def f():
     if not prev:
         prevp = preceding_leaf(p)
         if not prevp or prevp.type in OPENING_BRACKETS:
-
             return NO
 
         if prevp.type == token.EQUAL:
similarity index 91%
rename from .vim/bundle/black/tests/data/expression.diff
rename to .vim/bundle/black/tests/data/cases/expression.diff
index 721a07d2141ab71fc2d898302e7e7eccf44bd276..2eaaeb479f8c7334e75468642c01fa25c5d92f9c 100644 (file)
  True
  False
  1
-@@ -29,63 +29,96 @@
+@@ -21,99 +21,135 @@
+ Name1 or (Name2 and Name3) or Name4
+ Name1 or Name2 and Name3 or Name4
+ v1 << 2
+ 1 >> v2
+ 1 % finished
+-1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8
+-((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8)
++1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8
++((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8)
+ not great
  ~great
  +value
  -1
@@ -19,7 +29,7 @@
  (~int) and (not ((v1 ^ (123 + v2)) | True))
 -+really ** -confusing ** ~operator ** -precedence
 -flags & ~ select.EPOLLIN and waiters.write_task is not None
-++(really ** -(confusing ** ~(operator ** -precedence)))
+++(really ** -(confusing ** ~(operator**-precedence)))
 +flags & ~select.EPOLLIN and waiters.write_task is not None
  lambda arg: None
  lambda a=True: a
 +    *more,
 +]
  {i for i in (1, 2, 3)}
- {(i ** 2) for i in (1, 2, 3)}
+-{(i ** 2) for i in (1, 2, 3)}
 -{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}
-+{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
- {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
+-{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
++{(i**2) for i in (1, 2, 3)}
++{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
++{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
  [i for i in (1, 2, 3)]
- [(i ** 2) for i in (1, 2, 3)]
+-[(i ** 2) for i in (1, 2, 3)]
 -[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))]
-+[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
- [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
+-[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
++[(i**2) for i in (1, 2, 3)]
++[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
++[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
  {i: 0 for i in (1, 2, 3)}
 -{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}
 +{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))}
  call(**self.screen_kwargs)
  call(b, **self.screen_kwargs)
  lukasz.langa.pl
-@@ -94,26 +127,29 @@
- 1.0 .real
+ call.me(maybe)
+-1 .real
+-1.0 .real
++(1).real
++(1.0).real
  ....__class__
  list[str]
  dict[str, int]
  SomeName
  (Good, Bad, Ugly)
  (i for i in (1, 2, 3))
- ((i ** 2) for i in (1, 2, 3))
+-((i ** 2) for i in (1, 2, 3))
 -((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c')))
-+((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
- (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
+-(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
++((i**2) for i in (1, 2, 3))
++((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
++(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
  (*starred,)
 -{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs}
 +{
 +    return True
 +if (
 +    ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e
-+    | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n
++    | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n
 +):
 +    return True
 +if (
 +    ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e
 +    | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h
-+    ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n
++    ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n
 +):
 +    return True
 +if (
 +    | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h
 +    ^ aaaaaaaaaaaaaaaa.i
 +    << aaaaaaaaaaaaaaaa.k
-+    >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
++    >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
 +):
 +    return True
 +(
similarity index 95%
rename from .vim/bundle/black/tests/data/expression.py
rename to .vim/bundle/black/tests/data/cases/expression.py
index d13450cda68e81beba962c42e4c14d4c0f0938eb..06096c589f1791f9389f07b6ecef0f6b88299580 100644 (file)
@@ -282,15 +282,15 @@ Name1 or Name2 and Name3 or Name4
 v1 << 2
 1 >> v2
 1 % finished
-1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8
-((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8)
+1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8
+((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8)
 not great
 ~great
 +value
 -1
 ~int and not v1 ^ 123 + v2 | True
 (~int) and (not ((v1 ^ (123 + v2)) | True))
-+(really ** -(confusing ** ~(operator ** -precedence)))
++(really ** -(confusing ** ~(operator**-precedence)))
 flags & ~select.EPOLLIN and waiters.write_task is not None
 lambda arg: None
 lambda a=True: a
@@ -347,13 +347,13 @@ str or None if (1 if True else 2) else str or bytes or None
     *more,
 ]
 {i for i in (1, 2, 3)}
-{(i ** 2) for i in (1, 2, 3)}
-{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
-{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
+{(i**2) for i in (1, 2, 3)}
+{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
+{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
 [i for i in (1, 2, 3)]
-[(i ** 2) for i in (1, 2, 3)]
-[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
-[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
+[(i**2) for i in (1, 2, 3)]
+[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
+[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
 {i: 0 for i in (1, 2, 3)}
 {i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))}
 {a: b * 2 for a, b in dictionary.items()}
@@ -382,8 +382,8 @@ call(**self.screen_kwargs)
 call(b, **self.screen_kwargs)
 lukasz.langa.pl
 call.me(maybe)
-.real
-1.0 .real
+(1).real
+(1.0).real
 ....__class__
 list[str]
 dict[str, int]
@@ -441,9 +441,9 @@ numpy[np.newaxis, :]
 SomeName
 (Good, Bad, Ugly)
 (i for i in (1, 2, 3))
-((i ** 2) for i in (1, 2, 3))
-((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
-(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
+((i**2) for i in (1, 2, 3))
+((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
+(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
 (*starred,)
 {
     "id": "1",
@@ -588,13 +588,13 @@ if (
     return True
 if (
     ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e
-    | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n
+    | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n
 ):
     return True
 if (
     ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e
     | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h
-    ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n
+    ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n
 ):
     return True
 if (
@@ -604,7 +604,7 @@ if (
     | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h
     ^ aaaaaaaaaaaaaaaa.i
     << aaaaaaaaaaaaaaaa.k
-    >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
+    >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
 ):
     return True
 (
similarity index 100%
rename from .vim/bundle/black/tests/data/fmtonoff.py
rename to .vim/bundle/black/tests/data/cases/fmtonoff.py
index 5a50eb12ed32b21b31c836505dc6e94ffea2fd56..d1f15cd5c8b682c72de85e0a2e0b8d877ffe8730 100644 (file)
@@ -195,7 +195,6 @@ import sys
 from third_party import X, Y, Z
 
 from library import some_connection, some_decorator
-
 # fmt: off
 from third_party import (X,
                          Y, Z)
@@ -205,6 +204,7 @@ f"trigger 3.6 mode"
 
 # Comment 2
 
+
 # fmt: off
 def func_no_args():
   a; b; c
diff --git a/.vim/bundle/black/tests/data/cases/fmtonoff5.py b/.vim/bundle/black/tests/data/cases/fmtonoff5.py
new file mode 100644 (file)
index 0000000..181151b
--- /dev/null
@@ -0,0 +1,176 @@
+# Regression test for https://github.com/psf/black/issues/3129.
+setup(
+    entry_points={
+        # fmt: off
+        "console_scripts": [
+            "foo-bar"
+            "=foo.bar.:main",
+        # fmt: on
+            ]  # Includes an formatted indentation.
+    },
+)
+
+
+# Regression test for https://github.com/psf/black/issues/2015.
+run(
+    # fmt: off
+    [
+        "ls",
+        "-la",
+    ]
+    # fmt: on
+    + path,
+    check=True,
+)
+
+
+# Regression test for https://github.com/psf/black/issues/3026.
+def test_func():
+    # yapf: disable
+    if  unformatted(  args  ):
+        return True
+    # yapf: enable
+    elif b:
+        return True
+
+    return False
+
+
+# Regression test for https://github.com/psf/black/issues/2567.
+if True:
+    # fmt: off
+    for _ in range( 1 ):
+    # fmt: on
+        print ( "This won't be formatted" )
+    print ( "This won't be formatted either" )
+else:
+    print ( "This will be formatted" )
+
+
+# Regression test for https://github.com/psf/black/issues/3184.
+class A:
+    async def call(param):
+        if param:
+            # fmt: off
+            if param[0:4] in (
+                "ABCD", "EFGH"
+            )  :
+                # fmt: on
+                print ( "This won't be formatted" )
+
+            elif param[0:4] in ("ZZZZ",):
+                print ( "This won't be formatted either" )
+
+        print ( "This will be formatted" )
+
+
+# Regression test for https://github.com/psf/black/issues/2985.
+class Named(t.Protocol):
+    # fmt: off
+    @property
+    def  this_wont_be_formatted ( self ) -> str: ...
+
+class Factory(t.Protocol):
+    def  this_will_be_formatted ( self, **kwargs ) -> Named: ...
+    # fmt: on
+
+
+# Regression test for https://github.com/psf/black/issues/3436.
+if x:
+    return x
+# fmt: off
+elif   unformatted:
+# fmt: on
+    will_be_formatted  ()
+
+
+# output
+
+
+# Regression test for https://github.com/psf/black/issues/3129.
+setup(
+    entry_points={
+        # fmt: off
+        "console_scripts": [
+            "foo-bar"
+            "=foo.bar.:main",
+        # fmt: on
+            ]  # Includes an formatted indentation.
+    },
+)
+
+
+# Regression test for https://github.com/psf/black/issues/2015.
+run(
+    # fmt: off
+    [
+        "ls",
+        "-la",
+    ]
+    # fmt: on
+    + path,
+    check=True,
+)
+
+
+# Regression test for https://github.com/psf/black/issues/3026.
+def test_func():
+    # yapf: disable
+    if  unformatted(  args  ):
+        return True
+    # yapf: enable
+    elif b:
+        return True
+
+    return False
+
+
+# Regression test for https://github.com/psf/black/issues/2567.
+if True:
+    # fmt: off
+    for _ in range( 1 ):
+    # fmt: on
+        print ( "This won't be formatted" )
+    print ( "This won't be formatted either" )
+else:
+    print("This will be formatted")
+
+
+# Regression test for https://github.com/psf/black/issues/3184.
+class A:
+    async def call(param):
+        if param:
+            # fmt: off
+            if param[0:4] in (
+                "ABCD", "EFGH"
+            )  :
+                # fmt: on
+                print ( "This won't be formatted" )
+
+            elif param[0:4] in ("ZZZZ",):
+                print ( "This won't be formatted either" )
+
+        print("This will be formatted")
+
+
+# Regression test for https://github.com/psf/black/issues/2985.
+class Named(t.Protocol):
+    # fmt: off
+    @property
+    def  this_wont_be_formatted ( self ) -> str: ...
+
+
+class Factory(t.Protocol):
+    def this_will_be_formatted(self, **kwargs) -> Named:
+        ...
+
+    # fmt: on
+
+
+# Regression test for https://github.com/psf/black/issues/3436.
+if x:
+    return x
+# fmt: off
+elif   unformatted:
+    # fmt: on
+    will_be_formatted()
diff --git a/.vim/bundle/black/tests/data/cases/fmtpass_imports.py b/.vim/bundle/black/tests/data/cases/fmtpass_imports.py
new file mode 100644 (file)
index 0000000..8b3c0bc
--- /dev/null
@@ -0,0 +1,19 @@
+# Regression test for https://github.com/psf/black/issues/3438
+
+import ast
+import collections  # fmt: skip
+import dataclasses
+# fmt: off
+import os
+# fmt: on
+import pathlib
+
+import re  # fmt: skip
+import secrets
+
+# fmt: off
+import sys
+# fmt: on
+
+import tempfile
+import zoneinfo
diff --git a/.vim/bundle/black/tests/data/cases/fmtskip7.py b/.vim/bundle/black/tests/data/cases/fmtskip7.py
new file mode 100644 (file)
index 0000000..15ac0ad
--- /dev/null
@@ -0,0 +1,11 @@
+a =     "this is some code"
+b =     5  #fmt:skip
+c = 9  #fmt: skip
+d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring"  #fmt:skip
+
+# output
+
+a = "this is some code"
+b =     5  # fmt:skip
+c = 9  # fmt: skip
+d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring"  # fmt:skip
diff --git a/.vim/bundle/black/tests/data/cases/fmtskip8.py b/.vim/bundle/black/tests/data/cases/fmtskip8.py
new file mode 100644 (file)
index 0000000..38e9c2a
--- /dev/null
@@ -0,0 +1,62 @@
+# Make sure a leading comment is not removed.
+def some_func(  unformatted,  args  ):  # fmt: skip
+    print("I am some_func")
+    return 0
+    # Make sure this comment is not removed.
+
+
+# Make sure a leading comment is not removed.
+async def some_async_func(  unformatted,   args):  # fmt: skip
+    print("I am some_async_func")
+    await asyncio.sleep(1)
+
+
+# Make sure a leading comment is not removed.
+class SomeClass(  Unformatted,  SuperClasses  ):  # fmt: skip
+    def some_method(  self,  unformatted,  args  ):  # fmt: skip
+        print("I am some_method")
+        return 0
+
+    async def some_async_method(  self,  unformatted,  args  ):  # fmt: skip
+        print("I am some_async_method")
+        await asyncio.sleep(1)
+
+
+# Make sure a leading comment is not removed.
+if  unformatted_call(  args  ):  # fmt: skip
+    print("First branch")
+    # Make sure this is not removed.
+elif  another_unformatted_call(  args  ):  # fmt: skip
+    print("Second branch")
+else  :  # fmt: skip
+    print("Last branch")
+
+
+while  some_condition(  unformatted,  args  ):  # fmt: skip
+    print("Do something")
+
+
+for  i  in  some_iter(  unformatted,  args  ):  # fmt: skip
+    print("Do something")
+
+
+async def test_async_for():
+    async  for  i  in  some_async_iter(  unformatted,  args  ):  # fmt: skip
+        print("Do something")
+
+
+try  :  # fmt: skip
+    some_call()
+except  UnformattedError  as  ex:  # fmt: skip
+    handle_exception()
+finally  :  # fmt: skip
+    finally_call()
+
+
+with  give_me_context(  unformatted,  args  ):  # fmt: skip
+    print("Do something")
+
+
+async def test_async_with():
+    async  with  give_me_async_context(  unformatted,  args  ):  # fmt: skip
+        print("Do something")
similarity index 82%
rename from .vim/bundle/black/tests/data/fstring.py
rename to .vim/bundle/black/tests/data/cases/fstring.py
index 4b33231c01c08701c1f2fa47de9cfe8a1f0388c9..60560309376351e79d53d505dd3f89b1745eae7b 100644 (file)
@@ -7,6 +7,8 @@ f"{f'''{'nested'} inner'''} outer"
 f"\"{f'{nested} inner'}\" outer"
 f"space between opening braces: { {a for a in (1, 2, 3)}}"
 f'Hello \'{tricky + "example"}\''
+f"Tried directories {str(rootdirs)} \
+but none started with prefix {parentdir_prefix}"
 
 # output
 
@@ -19,3 +21,5 @@ f"{f'''{'nested'} inner'''} outer"
 f"\"{f'{nested} inner'}\" outer"
 f"space between opening braces: { {a for a in (1, 2, 3)}}"
 f'Hello \'{tricky + "example"}\''
+f"Tried directories {str(rootdirs)} \
+but none started with prefix {parentdir_prefix}"
diff --git a/.vim/bundle/black/tests/data/cases/funcdef_return_type_trailing_comma.py b/.vim/bundle/black/tests/data/cases/funcdef_return_type_trailing_comma.py
new file mode 100644 (file)
index 0000000..9b9b9c6
--- /dev/null
@@ -0,0 +1,301 @@
+# flags: --preview --minimum-version=3.10
+# normal, short, function definition
+def foo(a, b) -> tuple[int, float]: ...
+
+
+# normal, short, function definition w/o return type
+def foo(a, b): ...
+
+
+# no splitting
+def foo(a: A, b: B) -> list[p, q]:
+    pass
+
+
+# magic trailing comma in param list
+def foo(a, b,): ...
+
+
+# magic trailing comma in nested params in param list
+def foo(a, b: tuple[int, float,]): ...
+
+
+# magic trailing comma in return type, no params
+def a() -> tuple[
+    a,
+    b,
+]: ...
+
+
+# magic trailing comma in return type, params
+def foo(a: A, b: B) -> list[
+    p,
+    q,
+]:
+    pass
+
+
+# magic trailing comma in param list and in return type
+def foo(
+    a: a,
+    b: b,
+) -> list[
+    a,
+    a,
+]:
+    pass
+
+
+# long function definition, param list is longer
+def aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
+    bbbbbbbbbbbbbbbbbb,
+) -> cccccccccccccccccccccccccccccc: ...
+
+
+# long function definition, return type is longer
+# this should maybe split on rhs?
+def aaaaaaaaaaaaaaaaa(bbbbbbbbbbbbbbbbbb) -> list[
+    Ccccccccccccccccccccccccccccccccccccccccccccccccccc, Dddddd
+]: ...
+
+
+# long return type, no param list
+def foo() -> list[
+    Loooooooooooooooooooooooooooooooooooong,
+    Loooooooooooooooooooong,
+    Looooooooooooong,
+]: ...
+
+
+# long function name, no param list, no return value
+def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong():
+    pass
+
+
+# long function name, no param list
+def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong() -> (
+    list[int, float]
+): ...
+
+
+# long function name, no return value
+def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong(
+    a, b
+): ...
+
+
+# unskippable type hint (??)
+def foo(a) -> list[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]:  # type: ignore
+    pass
+
+
+def foo(a) -> list[
+    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+]:  # abpedeifnore
+    pass
+
+def foo(a, b: list[Bad],): ... # type: ignore
+
+# don't lose any comments (no magic)
+def foo( # 1
+    a, # 2
+    b) -> list[ # 3
+               a, # 4
+               b]: # 5
+        ... # 6
+
+
+# don't lose any comments (param list magic)
+def foo( # 1
+    a, # 2
+    b,) -> list[ # 3
+               a, # 4
+               b]: # 5
+        ... # 6
+
+
+# don't lose any comments (return type magic)
+def foo( # 1
+    a, # 2
+    b) -> list[ # 3
+               a, # 4
+               b,]: # 5
+        ... # 6
+
+
+# don't lose any comments (both magic)
+def foo( # 1
+    a, # 2
+    b,) -> list[ # 3
+               a, # 4
+               b,]: # 5
+        ... # 6
+
+# real life example
+def SimplePyFn(
+    context: hl.GeneratorContext,
+    buffer_input: Buffer[UInt8, 2],
+    func_input: Buffer[Int32, 2],
+    float_arg: Scalar[Float32],
+    offset: int = 0,
+) -> tuple[
+    Buffer[UInt8, 2],
+    Buffer[UInt8, 2],
+]: ...
+# output
+# normal, short, function definition
+def foo(a, b) -> tuple[int, float]: ...
+
+
+# normal, short, function definition w/o return type
+def foo(a, b): ...
+
+
+# no splitting
+def foo(a: A, b: B) -> list[p, q]:
+    pass
+
+
+# magic trailing comma in param list
+def foo(
+    a,
+    b,
+): ...
+
+
+# magic trailing comma in nested params in param list
+def foo(
+    a,
+    b: tuple[
+        int,
+        float,
+    ],
+): ...
+
+
+# magic trailing comma in return type, no params
+def a() -> tuple[
+    a,
+    b,
+]: ...
+
+
+# magic trailing comma in return type, params
+def foo(a: A, b: B) -> list[
+    p,
+    q,
+]:
+    pass
+
+
+# magic trailing comma in param list and in return type
+def foo(
+    a: a,
+    b: b,
+) -> list[
+    a,
+    a,
+]:
+    pass
+
+
+# long function definition, param list is longer
+def aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
+    bbbbbbbbbbbbbbbbbb,
+) -> cccccccccccccccccccccccccccccc: ...
+
+
+# long function definition, return type is longer
+# this should maybe split on rhs?
+def aaaaaaaaaaaaaaaaa(
+    bbbbbbbbbbbbbbbbbb,
+) -> list[Ccccccccccccccccccccccccccccccccccccccccccccccccccc, Dddddd]: ...
+
+
+# long return type, no param list
+def foo() -> list[
+    Loooooooooooooooooooooooooooooooooooong,
+    Loooooooooooooooooooong,
+    Looooooooooooong,
+]: ...
+
+
+# long function name, no param list, no return value
+def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong():
+    pass
+
+
+# long function name, no param list
+def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong() -> (
+    list[int, float]
+): ...
+
+
+# long function name, no return value
+def thiiiiiiiiiiiiiiiiiis_iiiiiiiiiiiiiiiiiiiiiiiiiiiiiis_veeeeeeeeeeeeeeeeeeeeeeery_looooooong(
+    a, b
+): ...
+
+
+# unskippable type hint (??)
+def foo(a) -> list[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]:  # type: ignore
+    pass
+
+
+def foo(
+    a,
+) -> list[
+    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+]:  # abpedeifnore
+    pass
+
+
+def foo(
+    a,
+    b: list[Bad],
+): ...  # type: ignore
+
+
+# don't lose any comments (no magic)
+def foo(a, b) -> list[a, b]:  # 1  # 2  # 3  # 4  # 5
+    ...  # 6
+
+
+# don't lose any comments (param list magic)
+def foo(  # 1
+    a,  # 2
+    b,
+) -> list[a, b]:  # 3  # 4  # 5
+    ...  # 6
+
+
+# don't lose any comments (return type magic)
+def foo(a, b) -> list[  # 1  # 2  # 3
+    a,  # 4
+    b,
+]:  # 5
+    ...  # 6
+
+
+# don't lose any comments (both magic)
+def foo(  # 1
+    a,  # 2
+    b,
+) -> list[  # 3
+    a,  # 4
+    b,
+]:  # 5
+    ...  # 6
+
+
+# real life example
+def SimplePyFn(
+    context: hl.GeneratorContext,
+    buffer_input: Buffer[UInt8, 2],
+    func_input: Buffer[Int32, 2],
+    float_arg: Scalar[Float32],
+    offset: int = 0,
+) -> tuple[
+    Buffer[UInt8, 2],
+    Buffer[UInt8, 2],
+]: ...
similarity index 52%
rename from .vim/bundle/black/tests/data/function2.py
rename to .vim/bundle/black/tests/data/cases/function2.py
index cfc259ea7bd7be7e645c606c4e6caed225007ed2..5bb36c26318cbd0b9992b55fd73da3485cf500c7 100644 (file)
@@ -23,6 +23,35 @@ def h():
         pass
     print("Inner defs should breathe a little.")
 
+
+if os.name == "posix":
+    import termios
+    def i_should_be_followed_by_only_one_newline():
+        pass
+elif os.name == "nt":
+    try:
+        import msvcrt
+        def i_should_be_followed_by_only_one_newline():
+            pass
+
+    except ImportError:
+
+        def i_should_be_followed_by_only_one_newline():
+            pass
+
+elif False:
+
+    class IHopeYouAreHavingALovelyDay:
+        def __call__(self):
+            print("i_should_be_followed_by_only_one_newline")
+else:
+
+    def foo():
+        pass
+
+with hmm_but_this_should_get_two_preceding_newlines():
+    pass
+
 # output
 
 def f(
@@ -56,3 +85,37 @@ def h():
         pass
 
     print("Inner defs should breathe a little.")
+
+
+if os.name == "posix":
+    import termios
+
+    def i_should_be_followed_by_only_one_newline():
+        pass
+
+elif os.name == "nt":
+    try:
+        import msvcrt
+
+        def i_should_be_followed_by_only_one_newline():
+            pass
+
+    except ImportError:
+
+        def i_should_be_followed_by_only_one_newline():
+            pass
+
+elif False:
+
+    class IHopeYouAreHavingALovelyDay:
+        def __call__(self):
+            print("i_should_be_followed_by_only_one_newline")
+
+else:
+
+    def foo():
+        pass
+
+
+with hmm_but_this_should_get_two_preceding_newlines():
+    pass
similarity index 68%
rename from .vim/bundle/black/tests/data/function_trailing_comma.py
rename to .vim/bundle/black/tests/data/cases/function_trailing_comma.py
index 02078219e8255f00559ac63391af8394bab63115..92f46e275160800bf00af9a8f95b3bfb4cc22a1e 100644 (file)
@@ -49,6 +49,17 @@ def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_cr
 ):
     pass
 
+
+# Make sure inner one-element tuple won't explode
+some_module.some_function(
+    argument1, (one_element_tuple,), argument4, argument5, argument6
+)
+
+# Inner trailing comma causes outer to explode
+some_module.some_function(
+    argument1, (one, two,), argument4, argument5, argument6
+)
+
 # output
 
 def f(
@@ -89,22 +100,25 @@ def f(
         "a": 1,
         "b": 2,
     }["a"]
-    if a == {
-        "a": 1,
-        "b": 2,
-        "c": 3,
-        "d": 4,
-        "e": 5,
-        "f": 6,
-        "g": 7,
-        "h": 8,
-    }["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"
-]:
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
+    Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]
+):
     json = {
         "k": {
             "k2": {
@@ -126,9 +140,7 @@ def some_function_with_a_really_long_name() -> (
 
 def some_method_with_a_really_long_name(
     very_long_parameter_so_yeah: str, another_long_parameter: int
-) -> (
-    another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not
-):
+) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not:
     pass
 
 
@@ -141,10 +153,26 @@ def func() -> (
 
 
 def func() -> (
-    (
-        also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(
-            this_shouldn_t_get_a_trailing_comma_too
-        )
+    also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(
+        this_shouldn_t_get_a_trailing_comma_too
     )
 ):
     pass
+
+
+# Make sure inner one-element tuple won't explode
+some_module.some_function(
+    argument1, (one_element_tuple,), argument4, argument5, argument6
+)
+
+# Inner trailing comma causes outer to explode
+some_module.some_function(
+    argument1,
+    (
+        one,
+        two,
+    ),
+    argument4,
+    argument5,
+    argument6,
+)
diff --git a/.vim/bundle/black/tests/data/cases/ignore_pyi.py b/.vim/bundle/black/tests/data/cases/ignore_pyi.py
new file mode 100644 (file)
index 0000000..4fae753
--- /dev/null
@@ -0,0 +1,42 @@
+# flags: --pyi
+def f():  # type: ignore
+    ...
+
+class x:  # some comment
+    ...
+
+class y:
+    ...  # comment
+
+# whitespace doesn't matter (note the next line has a trailing space and tab)
+class z:        
+    ...
+
+def g():
+    # hi
+    ...
+
+def h():
+    ...
+    # bye
+
+# output
+
+def f():  # type: ignore
+    ...
+
+class x:  # some comment
+    ...
+
+class y: ...  # comment
+
+# whitespace doesn't matter (note the next line has a trailing space and tab)
+class z: ...
+
+def g():
+    # hi
+    ...
+
+def h():
+    ...
+    # bye
diff --git a/.vim/bundle/black/tests/data/cases/linelength6.py b/.vim/bundle/black/tests/data/cases/linelength6.py
new file mode 100644 (file)
index 0000000..158038b
--- /dev/null
@@ -0,0 +1,6 @@
+# flags: --line-length=6
+# Regression test for #3427, which reproes only with line length <= 6
+def f():
+    """
+    x
+    """
similarity index 97%
rename from .vim/bundle/black/tests/data/long_strings_flag_disabled.py
rename to .vim/bundle/black/tests/data/cases/long_strings_flag_disabled.py
index ef3094fd77969401ff8f99067f61e3ec769fc2fc..db3954e3abd7ea472f3e090c11f5839f0bcc5c6c 100644 (file)
@@ -133,11 +133,14 @@ old_fmt_string2 = "This is a %s %s %s %s" % (
     "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!",
+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."
diff --git a/.vim/bundle/black/tests/data/cases/module_docstring_1.py b/.vim/bundle/black/tests/data/cases/module_docstring_1.py
new file mode 100644 (file)
index 0000000..d5897b4
--- /dev/null
@@ -0,0 +1,26 @@
+# flags: --preview
+"""Single line module-level docstring should be followed by single newline."""
+
+
+
+
+a = 1
+
+
+"""I'm just a string so should be followed by 2 newlines."""
+
+
+
+
+b = 2
+
+# output
+"""Single line module-level docstring should be followed by single newline."""
+
+a = 1
+
+
+"""I'm just a string so should be followed by 2 newlines."""
+
+
+b = 2
diff --git a/.vim/bundle/black/tests/data/cases/module_docstring_2.py b/.vim/bundle/black/tests/data/cases/module_docstring_2.py
new file mode 100644 (file)
index 0000000..e1f81b4
--- /dev/null
@@ -0,0 +1,68 @@
+# flags: --preview
+"""I am a very helpful module docstring.
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris
+nisi ut aliquip ex ea commodo consequat.
+Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur.
+Excepteur sint occaecat cupidatat non proident,
+sunt in culpa qui officia deserunt mollit anim id est laborum.
+"""
+
+
+
+
+a = 1
+
+
+"""Look at me I'm a docstring...
+
+............................................................
+............................................................
+............................................................
+............................................................
+............................................................
+............................................................
+............................................................
+........................................................NOT!
+"""
+
+
+
+
+b = 2
+
+# output
+"""I am a very helpful module docstring.
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris
+nisi ut aliquip ex ea commodo consequat.
+Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur.
+Excepteur sint occaecat cupidatat non proident,
+sunt in culpa qui officia deserunt mollit anim id est laborum.
+"""
+
+a = 1
+
+
+"""Look at me I'm a docstring...
+
+............................................................
+............................................................
+............................................................
+............................................................
+............................................................
+............................................................
+............................................................
+........................................................NOT!
+"""
+
+
+b = 2
diff --git a/.vim/bundle/black/tests/data/cases/module_docstring_3.py b/.vim/bundle/black/tests/data/cases/module_docstring_3.py
new file mode 100644 (file)
index 0000000..0631e13
--- /dev/null
@@ -0,0 +1,8 @@
+# flags: --preview
+"""Single line module-level docstring should be followed by single newline."""
+a = 1
+
+# output
+"""Single line module-level docstring should be followed by single newline."""
+
+a = 1
diff --git a/.vim/bundle/black/tests/data/cases/module_docstring_4.py b/.vim/bundle/black/tests/data/cases/module_docstring_4.py
new file mode 100644 (file)
index 0000000..515174d
--- /dev/null
@@ -0,0 +1,9 @@
+# flags: --preview
+"""Single line module-level docstring should be followed by single newline."""
+
+a = 1
+
+# output
+"""Single line module-level docstring should be followed by single newline."""
+
+a = 1
diff --git a/.vim/bundle/black/tests/data/cases/multiline_consecutive_open_parentheses_ignore.py b/.vim/bundle/black/tests/data/cases/multiline_consecutive_open_parentheses_ignore.py
new file mode 100644 (file)
index 0000000..6ec8bb4
--- /dev/null
@@ -0,0 +1,41 @@
+# This is a regression test. Issue #3737
+
+a = (  # type: ignore
+    int(  # type: ignore
+        int(  # type: ignore
+            int(  # type: ignore
+                6
+            )
+        )
+    )
+)
+
+b = (
+    int(
+        6
+    )
+)
+
+print(   "111") # type: ignore
+print(   "111"                         ) # type: ignore
+print(   "111"       ) # type: ignore
+
+
+# output
+
+
+# This is a regression test. Issue #3737
+
+a = (  # type: ignore
+    int(  # type: ignore
+        int(  # type: ignore
+            int(6)  # type: ignore
+        )
+    )
+)
+
+b = int(6)
+
+print("111")  # type: ignore
+print("111")  # type: ignore
+print("111")  # type: ignore
\ No newline at end of file
diff --git a/.vim/bundle/black/tests/data/cases/nested_stub.py b/.vim/bundle/black/tests/data/cases/nested_stub.py
new file mode 100644 (file)
index 0000000..b81549e
--- /dev/null
@@ -0,0 +1,44 @@
+# flags: --pyi --preview
+import sys
+
+class Outer:
+    class InnerStub: ...
+    outer_attr_after_inner_stub: int
+    class Inner:
+        inner_attr: int
+    outer_attr: int
+
+if sys.version_info > (3, 7):
+    if sys.platform == "win32":
+        assignment = 1
+        def function_definition(self): ...
+    def f1(self) -> str: ...
+    if sys.platform != "win32":
+        def function_definition(self): ...
+        assignment = 1
+    def f2(self) -> str: ...
+
+# output
+
+import sys
+
+class Outer:
+    class InnerStub: ...
+    outer_attr_after_inner_stub: int
+
+    class Inner:
+        inner_attr: int
+
+    outer_attr: int
+
+if sys.version_info > (3, 7):
+    if sys.platform == "win32":
+        assignment = 1
+        def function_definition(self): ...
+
+    def f1(self) -> str: ...
+    if sys.platform != "win32":
+        def function_definition(self): ...
+        assignment = 1
+
+    def f2(self) -> str: ...
\ No newline at end of file
similarity index 91%
rename from .vim/bundle/black/tests/data/numeric_literals.py
rename to .vim/bundle/black/tests/data/cases/numeric_literals.py
index 254da68d3308bf76cdf464dbf9aa5a5a74252828..996693287448580093d4be201fc770b6a985308c 100644 (file)
@@ -1,5 +1,3 @@
-#!/usr/bin/env python3.6
-
 x = 123456789
 x = 123456
 x = .1
@@ -21,9 +19,6 @@ x = 133333
 
 # output
 
-
-#!/usr/bin/env python3.6
-
 x = 123456789
 x = 123456
 x = 0.1
similarity index 77%
rename from .vim/bundle/black/tests/data/numeric_literals_skip_underscores.py
rename to .vim/bundle/black/tests/data/cases/numeric_literals_skip_underscores.py
index e345bb90276c709939125f2e366f45318e973486..6d60bdbb34d185dd63a79b1fcf579b8183ee78dc 100644 (file)
@@ -1,5 +1,3 @@
-#!/usr/bin/env python3.6
-
 x = 123456789
 x = 1_2_3_4_5_6_7
 x = 1E+1
@@ -11,8 +9,6 @@ x = 1_2.
 
 # output
 
-#!/usr/bin/env python3.6
-
 x = 123456789
 x = 1_2_3_4_5_6_7
 x = 1e1
diff --git a/.vim/bundle/black/tests/data/cases/one_element_subscript.py b/.vim/bundle/black/tests/data/cases/one_element_subscript.py
new file mode 100644 (file)
index 0000000..39205ba
--- /dev/null
@@ -0,0 +1,36 @@
+# We should not treat the trailing comma
+# in a single-element subscript.
+a: tuple[int,]
+b = tuple[int,]
+
+# The magic comma still applies to multi-element subscripts.
+c: tuple[int, int,]
+d = tuple[int, int,]
+
+# Magic commas still work as expected for non-subscripts.
+small_list = [1,]
+list_of_types = [tuple[int,],]
+
+# output
+# We should not treat the trailing comma
+# in a single-element subscript.
+a: tuple[int,]
+b = tuple[int,]
+
+# The magic comma still applies to multi-element subscripts.
+c: tuple[
+    int,
+    int,
+]
+d = tuple[
+    int,
+    int,
+]
+
+# Magic commas still work as expected for non-subscripts.
+small_list = [
+    1,
+]
+list_of_types = [
+    tuple[int,],
+]
diff --git a/.vim/bundle/black/tests/data/cases/parenthesized_context_managers.py b/.vim/bundle/black/tests/data/cases/parenthesized_context_managers.py
new file mode 100644 (file)
index 0000000..16645a1
--- /dev/null
@@ -0,0 +1,46 @@
+# flags: --minimum-version=3.10
+with (CtxManager() as example):
+    ...
+
+with (CtxManager1(), CtxManager2()):
+    ...
+
+with (CtxManager1() as example, CtxManager2()):
+    ...
+
+with (CtxManager1(), CtxManager2() as example):
+    ...
+
+with (CtxManager1() as example1, CtxManager2() as example2):
+    ...
+
+with (
+    CtxManager1() as example1,
+    CtxManager2() as example2,
+    CtxManager3() as example3,
+):
+    ...
+
+# output
+
+with CtxManager() as example:
+    ...
+
+with CtxManager1(), CtxManager2():
+    ...
+
+with CtxManager1() as example, CtxManager2():
+    ...
+
+with CtxManager1(), CtxManager2() as example:
+    ...
+
+with CtxManager1() as example1, CtxManager2() as example2:
+    ...
+
+with (
+    CtxManager1() as example1,
+    CtxManager2() as example2,
+    CtxManager3() as example3,
+):
+    ...
diff --git a/.vim/bundle/black/tests/data/cases/pattern_matching_complex.py b/.vim/bundle/black/tests/data/cases/pattern_matching_complex.py
new file mode 100644 (file)
index 0000000..10b4d26
--- /dev/null
@@ -0,0 +1,149 @@
+# flags: --minimum-version=3.10
+# Cases sampled from Lib/test/test_patma.py
+
+# case black_test_patma_098
+match x:
+    case -0j:
+        y = 0
+# case black_test_patma_142
+match x:
+    case bytes(z):
+        y = 0
+# case black_test_patma_073
+match x:
+    case 0 if 0:
+        y = 0
+    case 0 if 1:
+        y = 1
+# case black_test_patma_006
+match 3:
+    case 0 | 1 | 2 | 3:
+        x = True
+# case black_test_patma_049
+match x:
+    case [0, 1] | [1, 0]:
+        y = 0
+# case black_check_sequence_then_mapping
+match x:
+    case [*_]:
+        return "seq"
+    case {}:
+        return "map"
+# case black_test_patma_035
+match x:
+    case {0: [1, 2, {}]}:
+        y = 0
+    case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}:
+        y = 1
+    case []:
+        y = 2
+# case black_test_patma_107
+match x:
+    case 0.25 + 1.75j:
+        y = 0
+# case black_test_patma_097
+match x:
+    case -0j:
+        y = 0
+# case black_test_patma_007
+match 4:
+    case 0 | 1 | 2 | 3:
+        x = True
+# case black_test_patma_154
+match x:
+    case 0 if x:
+        y = 0
+# case black_test_patma_134
+match x:
+    case {1: 0}:
+        y = 0
+    case {0: 0}:
+        y = 1
+    case {**z}:
+        y = 2
+# case black_test_patma_185
+match Seq():
+    case [*_]:
+        y = 0
+# case black_test_patma_063
+match x:
+    case 1:
+        y = 0
+    case 1:
+        y = 1
+# case black_test_patma_248
+match x:
+    case {"foo": bar}:
+        y = bar
+# case black_test_patma_019
+match (0, 1, 2):
+    case [0, 1, *x, 2]:
+        y = 0
+# case black_test_patma_052
+match x:
+    case [0]:
+        y = 0
+    case [1, 0] if (x := x[:0]):
+        y = 1
+    case [1, 0]:
+        y = 2
+# case black_test_patma_191
+match w:
+    case [x, y, *_]:
+        z = 0
+# case black_test_patma_110
+match x:
+    case -0.25 - 1.75j:
+        y = 0
+# case black_test_patma_151
+match (x,):
+    case [y]:
+        z = 0
+# case black_test_patma_114
+match x:
+    case A.B.C.D:
+        y = 0
+# case black_test_patma_232
+match x:
+    case None:
+        y = 0
+# case black_test_patma_058
+match x:
+    case 0:
+        y = 0
+# case black_test_patma_233
+match x:
+    case False:
+        y = 0
+# case black_test_patma_078
+match x:
+    case []:
+        y = 0
+    case [""]:
+        y = 1
+    case "":
+        y = 2
+# case black_test_patma_156
+match x:
+    case z:
+        y = 0
+# case black_test_patma_189
+match w:
+    case [x, y, *rest]:
+        z = 0
+# case black_test_patma_042
+match x:
+    case (0 as z) | (1 as z) | (2 as z) if z == x % 2:
+        y = 0
+# case black_test_patma_034
+match x:
+    case {0: [1, 2, {}]}:
+        y = 0
+    case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}:
+        y = 1
+    case []:
+        y = 2
+# issue 3790
+match (X.type, Y):
+    case _:
+        pass
diff --git a/.vim/bundle/black/tests/data/cases/pattern_matching_extras.py b/.vim/bundle/black/tests/data/cases/pattern_matching_extras.py
new file mode 100644 (file)
index 0000000..1e1481d
--- /dev/null
@@ -0,0 +1,120 @@
+# flags: --minimum-version=3.10
+import match
+
+match something:
+    case [a as b]:
+        print(b)
+    case [a as b, c, d, e as f]:
+        print(f)
+    case Point(a as b):
+        print(b)
+    case Point(int() as x, int() as y):
+        print(x, y)
+
+
+match = 1
+case: int = re.match(something)
+
+match re.match(case):
+    case type("match", match):
+        pass
+    case match:
+        pass
+
+
+def func(match: case, case: match) -> case:
+    match Something():
+        case func(match, case):
+            ...
+        case another:
+            ...
+
+
+match maybe, multiple:
+    case perhaps, 5:
+        pass
+    case perhaps, 6,:
+        pass
+
+
+match more := (than, one), indeed,:
+    case _, (5, 6):
+        pass
+    case [[5], (6)], [7],:
+        pass
+    case _:
+        pass
+
+
+match a, *b, c:
+    case [*_]:
+        assert "seq" == _
+    case {}:
+        assert "map" == b
+
+
+match match(
+    case,
+    match(
+        match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match
+    ),
+    case,
+):
+    case case(
+        match=case,
+        case=re.match(
+            loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong
+        ),
+    ):
+        pass
+
+    case [a as match]:
+        pass
+
+    case case:
+        pass
+
+
+match match:
+    case case:
+        pass
+
+
+match a, *b(), c:
+    case d, *f, g:
+        pass
+
+
+match something:
+    case {
+        "key": key as key_1,
+        "password": PASS.ONE | PASS.TWO | PASS.THREE as password,
+    }:
+        pass
+    case {"maybe": something(complicated as this) as that}:
+        pass
+
+
+match something:
+    case 1 as a:
+        pass
+
+    case 2 as b, 3 as c:
+        pass
+
+    case 4 as d, (5 as e), (6 | 7 as g), *h:
+        pass
+
+
+match bar1:
+    case Foo(aa=Callable() as aa, bb=int()):
+        print(bar1.aa, bar1.bb)
+    case _:
+        print("no match", "\n")
+
+
+match bar1:
+    case Foo(
+        normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u
+    ):
+        pass
diff --git a/.vim/bundle/black/tests/data/cases/pattern_matching_generic.py b/.vim/bundle/black/tests/data/cases/pattern_matching_generic.py
new file mode 100644 (file)
index 0000000..4b4d45f
--- /dev/null
@@ -0,0 +1,108 @@
+# flags: --minimum-version=3.10
+re.match()
+match = a
+with match() as match:
+    match = f"{match}"
+
+re.match()
+match = a
+with match() as match:
+    match = f"{match}"
+
+
+def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
+    if not target_versions:
+        # No target_version specified, so try all grammars.
+        return [
+            # Python 3.7+
+            pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords,
+            # Python 3.0-3.6
+            pygram.python_grammar_no_print_statement_no_exec_statement,
+            # Python 2.7 with future print_function import
+            pygram.python_grammar_no_print_statement,
+            # Python 2.7
+            pygram.python_grammar,
+        ]
+
+    match match:
+        case case:
+            match match:
+                case case:
+                    pass
+
+    if all(version.is_python2() for version in target_versions):
+        # Python 2-only code, so try Python 2 grammars.
+        return [
+            # Python 2.7 with future print_function import
+            pygram.python_grammar_no_print_statement,
+            # Python 2.7
+            pygram.python_grammar,
+        ]
+
+    re.match()
+    match = a
+    with match() as match:
+        match = f"{match}"
+
+    def test_patma_139(self):
+        x = False
+        match x:
+            case bool(z):
+                y = 0
+        self.assertIs(x, False)
+        self.assertEqual(y, 0)
+        self.assertIs(z, x)
+
+    # Python 3-compatible code, so only try Python 3 grammar.
+    grammars = []
+    if supports_feature(target_versions, Feature.PATTERN_MATCHING):
+        # Python 3.10+
+        grammars.append(pygram.python_grammar_soft_keywords)
+    # If we have to parse both, try to parse async as a keyword first
+    if not supports_feature(
+        target_versions, Feature.ASYNC_IDENTIFIERS
+    ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING):
+        # Python 3.7-3.9
+        grammars.append(
+            pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords
+        )
+    if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
+        # Python 3.0-3.6
+        grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement)
+
+    def test_patma_155(self):
+        x = 0
+        y = None
+        match x:
+            case 1e1000:
+                y = 0
+        self.assertEqual(x, 0)
+        self.assertIs(y, None)
+
+        x = range(3)
+        match x:
+            case [y, case as x, z]:
+                w = 0
+
+    # At least one of the above branches must have been taken, because every Python
+    # version has exactly one of the two 'ASYNC_*' flags
+    return grammars
+
+
+def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node:
+    """Given a string with source, return the lib2to3 Node."""
+    if not src_txt.endswith("\n"):
+        src_txt += "\n"
+
+    grammars = get_grammars(set(target_versions))
+
+
+re.match()
+match = a
+with match() as match:
+    match = f"{match}"
+
+re.match()
+match = a
+with match() as match:
+    match = f"{match}"
diff --git a/.vim/bundle/black/tests/data/cases/pattern_matching_simple.py b/.vim/bundle/black/tests/data/cases/pattern_matching_simple.py
new file mode 100644 (file)
index 0000000..6fa2000
--- /dev/null
@@ -0,0 +1,93 @@
+# flags: --minimum-version=3.10
+# Cases sampled from PEP 636 examples
+
+match command.split():
+    case [action, obj]:
+        ...  # interpret action, obj
+
+match command.split():
+    case [action]:
+        ...  # interpret single-verb action
+    case [action, obj]:
+        ...  # interpret action, obj
+
+match command.split():
+    case ["quit"]:
+        print("Goodbye!")
+        quit_game()
+    case ["look"]:
+        current_room.describe()
+    case ["get", obj]:
+        character.get(obj, current_room)
+    case ["go", direction]:
+        current_room = current_room.neighbor(direction)
+    # The rest of your commands go here
+
+match command.split():
+    case ["drop", *objects]:
+        for obj in objects:
+            character.drop(obj, current_room)
+    # The rest of your commands go here
+
+match command.split():
+    case ["quit"]:
+        pass
+    case ["go", direction]:
+        print("Going:", direction)
+    case ["drop", *objects]:
+        print("Dropping: ", *objects)
+    case _:
+        print(f"Sorry, I couldn't understand {command!r}")
+
+match command.split():
+    case ["north"] | ["go", "north"]:
+        current_room = current_room.neighbor("north")
+    case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]:
+        ...  # Code for picking up the given object
+
+match command.split():
+    case ["go", ("north" | "south" | "east" | "west")]:
+        current_room = current_room.neighbor(...)
+        # how do I know which direction to go?
+
+match command.split():
+    case ["go", ("north" | "south" | "east" | "west") as direction]:
+        current_room = current_room.neighbor(direction)
+
+match command.split():
+    case ["go", direction] if direction in current_room.exits:
+        current_room = current_room.neighbor(direction)
+    case ["go", _]:
+        print("Sorry, you can't go that way")
+
+match event.get():
+    case Click(position=(x, y)):
+        handle_click_at(x, y)
+    case KeyPress(key_name="Q") | Quit():
+        game.quit()
+    case KeyPress(key_name="up arrow"):
+        game.go_north()
+    case KeyPress():
+        pass  # Ignore other keystrokes
+    case other_event:
+        raise ValueError(f"Unrecognized event: {other_event}")
+
+match event.get():
+    case Click((x, y), button=Button.LEFT):  # This is a left click
+        handle_click_at(x, y)
+    case Click():
+        pass  # ignore other clicks
+
+
+def where_is(point):
+    match point:
+        case Point(x=0, y=0):
+            print("Origin")
+        case Point(x=0, y=y):
+            print(f"Y={y}")
+        case Point(x=x, y=0):
+            print(f"X={x}")
+        case Point():
+            print("Somewhere else")
+        case _:
+            print("Not a point")
diff --git a/.vim/bundle/black/tests/data/cases/pattern_matching_style.py b/.vim/bundle/black/tests/data/cases/pattern_matching_style.py
new file mode 100644 (file)
index 0000000..2ee6ea2
--- /dev/null
@@ -0,0 +1,92 @@
+# flags: --minimum-version=3.10
+match something:
+    case b(): print(1+1)
+    case c(
+        very_complex=True,
+        perhaps_even_loooooooooooooooooooooooooooooooooooooong=-   1
+    ): print(1)
+    case c(
+        very_complex=True,
+        perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1,
+    ): print(2)
+    case a: pass
+
+match(
+    arg # comment
+)
+
+match(
+)
+
+match(
+
+
+)
+
+case(
+    arg # comment
+)
+
+case(
+)
+
+case(
+
+
+)
+
+
+re.match(
+    something # fast
+)
+re.match(
+
+
+
+)
+match match(
+
+
+):
+    case case(
+        arg, # comment
+    ):
+        pass
+
+# output
+
+match something:
+    case b():
+        print(1 + 1)
+    case c(
+        very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1
+    ):
+        print(1)
+    case c(
+        very_complex=True,
+        perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1,
+    ):
+        print(2)
+    case a:
+        pass
+
+match(arg)  # comment
+
+match()
+
+match()
+
+case(arg)  # comment
+
+case()
+
+case()
+
+
+re.match(something)  # fast
+re.match()
+match match():
+    case case(
+        arg,  # comment
+    ):
+        pass
diff --git a/.vim/bundle/black/tests/data/cases/pep604_union_types_line_breaks.py b/.vim/bundle/black/tests/data/cases/pep604_union_types_line_breaks.py
new file mode 100644 (file)
index 0000000..fee2b84
--- /dev/null
@@ -0,0 +1,188 @@
+# flags: --preview --minimum-version=3.10
+# This has always worked
+z= Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong
+
+# "AnnAssign"s now also work
+z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong
+z: (Short
+    | Short2
+    | Short3
+    | Short4)
+z: (int)
+z: ((int))
+
+
+z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7
+z: (Short
+    | Short2
+    | Short3
+    | Short4) = 8
+z: (int) = 2.3
+z: ((int)) = foo()
+
+# In case I go for not enforcing parantheses, this might get improved at the same time
+x = (
+    z
+    == 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999,
+    y
+    == 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999,
+)
+
+x = (
+    z == (9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999),
+    y == (9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999),
+)
+
+# handle formatting of "tname"s in parameter list
+
+# remove unnecessary paren
+def foo(i: (int)) -> None: ...
+
+
+# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so.
+def foo(i: (int,)) -> None: ...
+
+def foo(
+    i: int,
+    x: Loooooooooooooooooooooooong
+    | Looooooooooooooooong
+    | Looooooooooooooooooooong
+    | Looooooong,
+    *,
+    s: str,
+) -> None:
+    pass
+
+
+@app.get("/path/")
+async def foo(
+    q: str
+    | None = Query(None, title="Some long title", description="Some long description")
+):
+    pass
+
+
+def f(
+    max_jobs: int
+    | None = Option(
+        None, help="Maximum number of jobs to launch. And some additional text."
+        ),
+    another_option: bool = False
+    ):
+    ...
+
+
+# output
+# This has always worked
+z = (
+    Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+)
+
+# "AnnAssign"s now also work
+z: (
+    Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+)
+z: Short | Short2 | Short3 | Short4
+z: int
+z: int
+
+
+z: (
+    Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+    | Loooooooooooooooooooooooong
+) = 7
+z: Short | Short2 | Short3 | Short4 = 8
+z: int = 2.3
+z: int = foo()
+
+# In case I go for not enforcing parantheses, this might get improved at the same time
+x = (
+    z
+    == 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999
+    | 9999999999999999999999999999999999999999,
+    y
+    == 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999
+    + 9999999999999999999999999999999999999999,
+)
+
+x = (
+    z
+    == (
+        9999999999999999999999999999999999999999
+        | 9999999999999999999999999999999999999999
+        | 9999999999999999999999999999999999999999
+        | 9999999999999999999999999999999999999999
+    ),
+    y
+    == (
+        9999999999999999999999999999999999999999
+        + 9999999999999999999999999999999999999999
+        + 9999999999999999999999999999999999999999
+        + 9999999999999999999999999999999999999999
+    ),
+)
+
+# handle formatting of "tname"s in parameter list
+
+
+# remove unnecessary paren
+def foo(i: int) -> None: ...
+
+
+# this is a syntax error in the type annotation according to mypy, but it's not invalid *python* code, so make sure we don't mess with it and make it so.
+def foo(i: (int,)) -> None: ...
+
+
+def foo(
+    i: int,
+    x: (
+        Loooooooooooooooooooooooong
+        | Looooooooooooooooong
+        | Looooooooooooooooooooong
+        | Looooooong
+    ),
+    *,
+    s: str,
+) -> None:
+    pass
+
+
+@app.get("/path/")
+async def foo(
+    q: str | None = Query(
+        None, title="Some long title", description="Some long description"
+    )
+):
+    pass
+
+
+def f(
+    max_jobs: int | None = Option(
+        None, help="Maximum number of jobs to launch. And some additional text."
+    ),
+    another_option: bool = False,
+): ...
similarity index 95%
rename from .vim/bundle/black/tests/data/pep_570.py
rename to .vim/bundle/black/tests/data/cases/pep_570.py
index ca8f7ab1d95d08f642db68a852f2821c09a9c720..2641c2b970edb77adb82a1fbeaad3fd48bbf3e63 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --minimum-version=3.8
 def positional_only_arg(a, /):
     pass
 
similarity index 94%
rename from .vim/bundle/black/tests/data/pep_572.py
rename to .vim/bundle/black/tests/data/cases/pep_572.py
index c6867f2625855693197e2d262120193788528b7c..742b6d5b7e41eed7d047cfa2c5ec5d221c59358c 100644 (file)
@@ -1,10 +1,11 @@
+# flags: --minimum-version=3.8
 (a := 1)
 (a := a)
 if (match := pattern.search(data)) is None:
     pass
 if match := pattern.search(data):
     pass
-[y := f(x), y ** 2, y ** 3]
+[y := f(x), y**2, y**3]
 filtered_data = [y for x in data if (y := f(x)) is None]
 (y := f(x))
 y0 = (y1 := f(x))
similarity index 89%
rename from .vim/bundle/black/tests/data/pep_572_do_not_remove_parens.py
rename to .vim/bundle/black/tests/data/cases/pep_572_do_not_remove_parens.py
index 20e80a693774a50efb6f969fe6de3f3eeda6370f..08dba3ffdf9b3eacc1c4410452bd631f06085384 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --fast
 # Most of the following examples are really dumb, some of them aren't even accepted by Python,
 # we're fixing them only so fuzzers (which follow the grammar which actually allows these
 # examples matter of fact!) don't yell at us :p
@@ -19,3 +20,7 @@ with (y := [3, 2, 1]) as (funfunfun := indeed):
 @(please := stop)
 def sigh():
     pass
+
+
+for (x := 3, y := 4) in y:
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/pep_572_py310.py b/.vim/bundle/black/tests/data/cases/pep_572_py310.py
new file mode 100644 (file)
index 0000000..9f999de
--- /dev/null
@@ -0,0 +1,16 @@
+# flags: --minimum-version=3.10
+# Unparenthesized walruses are now allowed in indices since Python 3.10.
+x[a:=0]
+x[a:=0, b:=1]
+x[5, b:=0]
+
+# Walruses are allowed inside generator expressions on function calls since 3.10.
+if any(match := pattern_error.match(s) for s in buffer):
+    if match.group(2) == data_not_available:
+        # Error OK to ignore.
+        pass
+
+f(a := b + c for c in range(10))
+f((a := b + c for c in range(10)), x)
+f(y=(a := b + c for c in range(10)))
+f(x, (a := b + c for c in range(10)), y=z, **q)
similarity index 78%
rename from .vim/bundle/black/tests/data/pep_572_py39.py
rename to .vim/bundle/black/tests/data/cases/pep_572_py39.py
index 7bbd509119729979974c46e164aee0ea43dcb1bc..d1614624d99448a5e6ea7b6a02a518da65280755 100644 (file)
@@ -1,7 +1,8 @@
+# flags: --minimum-version=3.9
 # Unparenthesized walruses are now allowed in set literals & set comprehensions
 # since Python 3.9
 {x := 1, 2, 3}
-{x4 := x ** 5 for x in range(7)}
+{x4 := x**5 for x in range(7)}
 # We better not remove the parentheses here (since it's a 3.10 feature)
 x[(a := 1)]
 x[(a := 1), (b := 3)]
similarity index 65%
rename from .vim/bundle/black/tests/data/pep_572_remove_parens.py
rename to .vim/bundle/black/tests/data/cases/pep_572_remove_parens.py
index 9718d95b499de4e2c966de31d10433deb471e681..24f1ac29168f7069e8d3563080522bf80ffe1984 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --minimum-version=3.8
 if (foo := 0):
     pass
 
@@ -49,6 +50,25 @@ def a():
 def this_is_so_dumb() -> (please := no):
     pass
 
+async def await_the_walrus():
+    with (x := y):
+        pass
+
+    with (x := y) as z, (a := b) as c:
+        pass
+
+    with (x := await y):
+        pass
+
+    with (x := await a, y := await b):
+        pass
+
+    with ((x := await a, y := await b)):
+        pass
+
+    with (x := await a), (y := await b):
+        pass
+
 
 # output
 if foo := 0:
@@ -103,3 +123,22 @@ def a():
 def this_is_so_dumb() -> (please := no):
     pass
 
+
+async def await_the_walrus():
+    with (x := y):
+        pass
+
+    with (x := y) as z, (a := b) as c:
+        pass
+
+    with (x := await y):
+        pass
+
+    with (x := await a, y := await b):
+        pass
+
+    with (x := await a, y := await b):
+        pass
+
+    with (x := await a), (y := await b):
+        pass
diff --git a/.vim/bundle/black/tests/data/cases/pep_604.py b/.vim/bundle/black/tests/data/cases/pep_604.py
new file mode 100644 (file)
index 0000000..b68d59d
--- /dev/null
@@ -0,0 +1,25 @@
+def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None:
+    pass
+
+
+def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | my_module.EvenMoreType | None:
+    pass
+
+
+# output
+
+
+def some_very_long_name_function() -> (
+    my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None
+):
+    pass
+
+
+def some_very_long_name_function() -> (
+    my_module.Asdf
+    | my_module.AnotherType
+    | my_module.YetAnotherType
+    | my_module.EvenMoreType
+    | None
+):
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/pep_646.py b/.vim/bundle/black/tests/data/cases/pep_646.py
new file mode 100644 (file)
index 0000000..92b568a
--- /dev/null
@@ -0,0 +1,195 @@
+# flags: --minimum-version=3.11
+A[*b]
+A[*b] = 1
+A
+del A[*b]
+A
+A[*b, *b]
+A[*b, *b] = 1
+A
+del A[*b, *b]
+A
+A[b, *b]
+A[b, *b] = 1
+A
+del A[b, *b]
+A
+A[*b, b]
+A[*b, b] = 1
+A
+del A[*b, b]
+A
+A[b, b, *b]
+A[b, b, *b] = 1
+A
+del A[b, b, *b]
+A
+A[*b, b, b]
+A[*b, b, b] = 1
+A
+del A[*b, b, b]
+A
+A[b, *b, b]
+A[b, *b, b] = 1
+A
+del A[b, *b, b]
+A
+A[b, b, *b, b]
+A[b, b, *b, b] = 1
+A
+del A[b, b, *b, b]
+A
+A[b, *b, b, b]
+A[b, *b, b, b] = 1
+A
+del A[b, *b, b, b]
+A
+A[A[b, *b, b]]
+A[A[b, *b, b]] = 1
+A
+del A[A[b, *b, b]]
+A
+A[*A[b, *b, b]]
+A[*A[b, *b, b]] = 1
+A
+del A[*A[b, *b, b]]
+A
+A[b, ...]
+A[b, ...] = 1
+A
+del A[b, ...]
+A
+A[*A[b, ...]]
+A[*A[b, ...]] = 1
+A
+del A[*A[b, ...]]
+A
+l = [1, 2, 3]
+A[*l]
+A[*l] = 1
+A
+del A[*l]
+A
+A[*l, 4]
+A[*l, 4] = 1
+A
+del A[*l, 4]
+A
+A[0, *l]
+A[0, *l] = 1
+A
+del A[0, *l]
+A
+A[1:2, *l]
+A[1:2, *l] = 1
+A
+del A[1:2, *l]
+A
+repr(A[1:2, *l]) == repr(A[1:2, 1, 2, 3])
+t = (1, 2, 3)
+A[*t]
+A[*t] = 1
+A
+del A[*t]
+A
+A[*t, 4]
+A[*t, 4] = 1
+A
+del A[*t, 4]
+A
+A[0, *t]
+A[0, *t] = 1
+A
+del A[0, *t]
+A
+A[1:2, *t]
+A[1:2, *t] = 1
+A
+del A[1:2, *t]
+A
+repr(A[1:2, *t]) == repr(A[1:2, 1, 2, 3])
+
+
+def returns_list():
+    return [1, 2, 3]
+
+
+A[returns_list()]
+A[returns_list()] = 1
+A
+del A[returns_list()]
+A
+A[returns_list(), 4]
+A[returns_list(), 4] = 1
+A
+del A[returns_list(), 4]
+A
+A[*returns_list()]
+A[*returns_list()] = 1
+A
+del A[*returns_list()]
+A
+A[*returns_list(), 4]
+A[*returns_list(), 4] = 1
+A
+del A[*returns_list(), 4]
+A
+A[0, *returns_list()]
+A[0, *returns_list()] = 1
+A
+del A[0, *returns_list()]
+A
+A[*returns_list(), *returns_list()]
+A[*returns_list(), *returns_list()] = 1
+A
+del A[*returns_list(), *returns_list()]
+A
+A[1:2, *b]
+A[*b, 1:2]
+A[1:2, *b, 1:2]
+A[*b, 1:2, *b]
+A[1:, *b]
+A[*b, 1:]
+A[1:, *b, 1:]
+A[*b, 1:, *b]
+A[:1, *b]
+A[*b, :1]
+A[:1, *b, :1]
+A[*b, :1, *b]
+A[:, *b]
+A[*b, :]
+A[:, *b, :]
+A[*b, :, *b]
+A[a * b()]
+A[a * b(), *c, *d(), e * f(g * h)]
+A[a * b(), :]
+A[a * b(), *c, *d(), e * f(g * h) :]
+A[[b] * len(c), :]
+
+
+def f1(*args: *b):
+    pass
+
+
+f1.__annotations__
+
+
+def f2(*args: *b, arg1):
+    pass
+
+
+f2.__annotations__
+
+
+def f3(*args: *b, arg1: int):
+    pass
+
+
+f3.__annotations__
+
+
+def f4(*args: *b, arg1: int = 2):
+    pass
+
+
+f4.__annotations__
diff --git a/.vim/bundle/black/tests/data/cases/pep_654.py b/.vim/bundle/black/tests/data/cases/pep_654.py
new file mode 100644 (file)
index 0000000..12e4918
--- /dev/null
@@ -0,0 +1,54 @@
+# flags: --minimum-version=3.11
+try:
+    raise OSError("blah")
+except* ExceptionGroup as e:
+    pass
+
+
+try:
+    async with trio.open_nursery() as nursery:
+        # Make two concurrent calls to child()
+        nursery.start_soon(child)
+        nursery.start_soon(child)
+except* ValueError:
+    pass
+
+try:
+    try:
+        raise ValueError(42)
+    except:
+        try:
+            raise TypeError(int)
+        except* Exception:
+            pass
+        1 / 0
+except Exception as e:
+    exc = e
+
+try:
+    try:
+        raise FalsyEG("eg", [TypeError(1), ValueError(2)])
+    except* TypeError as e:
+        tes = e
+        raise
+    except* ValueError as e:
+        ves = e
+        pass
+except Exception as e:
+    exc = e
+
+try:
+    try:
+        raise orig
+    except* (TypeError, ValueError) as e:
+        raise SyntaxError(3) from e
+except BaseException as e:
+    exc = e
+
+try:
+    try:
+        raise orig
+    except* OSError as e:
+        raise TypeError(3) from e
+except ExceptionGroup as e:
+    exc = e
diff --git a/.vim/bundle/black/tests/data/cases/pep_654_style.py b/.vim/bundle/black/tests/data/cases/pep_654_style.py
new file mode 100644 (file)
index 0000000..0d34650
--- /dev/null
@@ -0,0 +1,112 @@
+# flags: --minimum-version=3.11
+try:
+    raise OSError("blah")
+except               * ExceptionGroup as e:
+    pass
+
+
+try:
+    async with trio.open_nursery() as nursery:
+        # Make two concurrent calls to child()
+        nursery.start_soon(child)
+        nursery.start_soon(child)
+except *ValueError:
+    pass
+
+try:
+    try:
+        raise ValueError(42)
+    except:
+        try:
+            raise TypeError(int)
+        except *(Exception):
+            pass
+        1 / 0
+except Exception as e:
+    exc = e
+
+try:
+    try:
+        raise FalsyEG("eg", [TypeError(1), ValueError(2)])
+    except \
+        *TypeError as e:
+        tes = e
+        raise
+    except  *  ValueError as e:
+        ves = e
+        pass
+except Exception as e:
+    exc = e
+
+try:
+    try:
+        raise orig
+    except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e:
+        raise SyntaxError(3) from e
+except BaseException as e:
+    exc = e
+
+try:
+    try:
+        raise orig
+    except\
+        * OSError as e:
+        raise TypeError(3) from e
+except ExceptionGroup as e:
+    exc = e
+
+# output
+
+try:
+    raise OSError("blah")
+except* ExceptionGroup as e:
+    pass
+
+
+try:
+    async with trio.open_nursery() as nursery:
+        # Make two concurrent calls to child()
+        nursery.start_soon(child)
+        nursery.start_soon(child)
+except* ValueError:
+    pass
+
+try:
+    try:
+        raise ValueError(42)
+    except:
+        try:
+            raise TypeError(int)
+        except* Exception:
+            pass
+        1 / 0
+except Exception as e:
+    exc = e
+
+try:
+    try:
+        raise FalsyEG("eg", [TypeError(1), ValueError(2)])
+    except* TypeError as e:
+        tes = e
+        raise
+    except* ValueError as e:
+        ves = e
+        pass
+except Exception as e:
+    exc = e
+
+try:
+    try:
+        raise orig
+    except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e:
+        raise SyntaxError(3) from e
+except BaseException as e:
+    exc = e
+
+try:
+    try:
+        raise orig
+    except* OSError as e:
+        raise TypeError(3) from e
+except ExceptionGroup as e:
+    exc = e
diff --git a/.vim/bundle/black/tests/data/cases/power_op_newline.py b/.vim/bundle/black/tests/data/cases/power_op_newline.py
new file mode 100644 (file)
index 0000000..d9b3140
--- /dev/null
@@ -0,0 +1,11 @@
+# flags: --line-length=0
+importA;()<<0**0#
+
+# output
+
+importA
+(
+    ()
+    << 0
+    ** 0
+)  #
diff --git a/.vim/bundle/black/tests/data/cases/power_op_spacing.py b/.vim/bundle/black/tests/data/cases/power_op_spacing.py
new file mode 100644 (file)
index 0000000..b3ef0aa
--- /dev/null
@@ -0,0 +1,149 @@
+def function(**kwargs):
+    t = a**2 + b**3
+    return t ** 2
+
+
+def function_replace_spaces(**kwargs):
+    t = a **2 + b** 3 + c ** 4
+
+
+def function_dont_replace_spaces():
+    {**a, **b, **c}
+
+
+a = 5**~4
+b = 5 ** f()
+c = -(5**2)
+d = 5 ** f["hi"]
+e = lazy(lambda **kwargs: 5)
+f = f() ** 5
+g = a.b**c.d
+h = 5 ** funcs.f()
+i = funcs.f() ** 5
+j = super().name ** 5
+k = [(2**idx, value) for idx, value in pairs]
+l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
+m = [([2**63], [1, 2**63])]
+n = count <= 10**5
+o = settings(max_examples=10**6)
+p = {(k, k**2): v**2 for k, v in pairs}
+q = [10**i for i in range(6)]
+r = x**y
+s = 1 ** 1
+t = (
+    1
+    ** 1
+    **1
+    ** 1
+)
+
+a = 5.0**~4.0
+b = 5.0 ** f()
+c = -(5.0**2.0)
+d = 5.0 ** f["hi"]
+e = lazy(lambda **kwargs: 5)
+f = f() ** 5.0
+g = a.b**c.d
+h = 5.0 ** funcs.f()
+i = funcs.f() ** 5.0
+j = super().name ** 5.0
+k = [(2.0**idx, value) for idx, value in pairs]
+l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
+m = [([2.0**63.0], [1.0, 2**63.0])]
+n = count <= 10**5.0
+o = settings(max_examples=10**6.0)
+p = {(k, k**2): v**2.0 for k, v in pairs}
+q = [10.5**i for i in range(6)]
+s = 1.0 ** 1.0
+t = (
+    1.0
+    ** 1.0
+    **1.0
+    ** 1.0
+)
+
+
+# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
+if hasattr(view, "sum_of_weights"):
+    return np.divide(  # type: ignore[no-any-return]
+        view.variance,  # type: ignore[union-attr]
+        view.sum_of_weights,  # type: ignore[union-attr]
+        out=np.full(view.sum_of_weights.shape, np.nan),  # type: ignore[union-attr]
+        where=view.sum_of_weights**2 > view.sum_of_weights_squared,  # type: ignore[union-attr]
+    )
+
+return np.divide(
+    where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared,  # type: ignore
+)
+
+
+# output
+
+
+def function(**kwargs):
+    t = a**2 + b**3
+    return t**2
+
+
+def function_replace_spaces(**kwargs):
+    t = a**2 + b**3 + c**4
+
+
+def function_dont_replace_spaces():
+    {**a, **b, **c}
+
+
+a = 5**~4
+b = 5 ** f()
+c = -(5**2)
+d = 5 ** f["hi"]
+e = lazy(lambda **kwargs: 5)
+f = f() ** 5
+g = a.b**c.d
+h = 5 ** funcs.f()
+i = funcs.f() ** 5
+j = super().name ** 5
+k = [(2**idx, value) for idx, value in pairs]
+l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
+m = [([2**63], [1, 2**63])]
+n = count <= 10**5
+o = settings(max_examples=10**6)
+p = {(k, k**2): v**2 for k, v in pairs}
+q = [10**i for i in range(6)]
+r = x**y
+s = 1**1
+t = 1**1**1**1
+
+a = 5.0**~4.0
+b = 5.0 ** f()
+c = -(5.0**2.0)
+d = 5.0 ** f["hi"]
+e = lazy(lambda **kwargs: 5)
+f = f() ** 5.0
+g = a.b**c.d
+h = 5.0 ** funcs.f()
+i = funcs.f() ** 5.0
+j = super().name ** 5.0
+k = [(2.0**idx, value) for idx, value in pairs]
+l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
+m = [([2.0**63.0], [1.0, 2**63.0])]
+n = count <= 10**5.0
+o = settings(max_examples=10**6.0)
+p = {(k, k**2): v**2.0 for k, v in pairs}
+q = [10.5**i for i in range(6)]
+s = 1.0**1.0
+t = 1.0**1.0**1.0**1.0
+
+
+# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
+if hasattr(view, "sum_of_weights"):
+    return np.divide(  # type: ignore[no-any-return]
+        view.variance,  # type: ignore[union-attr]
+        view.sum_of_weights,  # type: ignore[union-attr]
+        out=np.full(view.sum_of_weights.shape, np.nan),  # type: ignore[union-attr]
+        where=view.sum_of_weights**2 > view.sum_of_weights_squared,  # type: ignore[union-attr]
+    )
+
+return np.divide(
+    where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared,  # type: ignore
+)
diff --git a/.vim/bundle/black/tests/data/cases/prefer_rhs_split_reformatted.py b/.vim/bundle/black/tests/data/cases/prefer_rhs_split_reformatted.py
new file mode 100644 (file)
index 0000000..e15e5dd
--- /dev/null
@@ -0,0 +1,38 @@
+# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
+
+# Left hand side fits in a single line but will still be exploded by the
+# magic trailing comma.
+first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
+    arg1,
+    arg2,
+)
+
+# Make when when the left side of assignment plus the opening paren "... = (" is
+# exactly line length limit + 1, it won't be split like that.
+xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1
+
+
+# output
+
+
+# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
+
+# Left hand side fits in a single line but will still be exploded by the
+# magic trailing comma.
+(
+    first_value,
+    (
+        m1,
+        m2,
+    ),
+    third_value,
+) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
+    arg1,
+    arg2,
+)
+
+# Make when when the left side of assignment plus the opening paren "... = (" is
+# exactly line length limit + 1, it won't be split like that.
+xxxxxxxxx_yyy_zzzzzzzz[
+    xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
+] = 1
diff --git a/.vim/bundle/black/tests/data/cases/preview_async_stmts.py b/.vim/bundle/black/tests/data/cases/preview_async_stmts.py
new file mode 100644 (file)
index 0000000..0a7671b
--- /dev/null
@@ -0,0 +1,28 @@
+# flags: --preview
+async def func() -> (int):
+    return 0
+
+
+@decorated
+async def func() -> (int):
+    return 0
+
+
+async for (item) in async_iter:
+    pass
+
+
+# output
+
+
+async def func() -> int:
+    return 0
+
+
+@decorated
+async def func() -> int:
+    return 0
+
+
+async for item in async_iter:
+    pass
similarity index 99%
rename from .vim/bundle/black/tests/data/cantfit.py
rename to .vim/bundle/black/tests/data/cases/preview_cantfit.py
index 0849374f776ebbefc13f0332dd55aaeb0e690feb..d5da6654f0c2503acf74844f69e3e0b9b0c1bb26 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --preview
 # long variable name
 this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 0
 this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 1  # with a comment
similarity index 74%
rename from .vim/bundle/black/tests/data/comments7.py
rename to .vim/bundle/black/tests/data/cases/preview_comments7.py
index ca9d7c62b215515e57c21a0ef55dd8ad07174c36..006d4f7266f8e872ba41cae9f5d6dbe95b315421 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --preview
 from .config import (
     Any,
     Bool,
@@ -131,6 +132,18 @@ class C:
 
 square = Square(4) # type: Optional[Square]
 
+# Regression test for https://github.com/psf/black/issues/3756.
+[
+    (
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"  # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+    ),
+]
+[
+    (  # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"  # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+    ),
+]
+
 # output
 
 from .config import (
@@ -226,46 +239,71 @@ class C:
             # metadata_version errors.
             (
                 {},
-                "None is an invalid value for Metadata-Version. Error: This field is"
-                " required. see"
-                " https://packaging.python.org/specifications/core-metadata",
+                (
+                    "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",
+                (
+                    "'-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",
+                (
+                    "'' 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",
+                (
+                    "'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",
+                (
+                    "'' 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",
+                (
+                    "'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
-    ):
-        ...
+    ): ...
 
 
 square = Square(4)  # type: Optional[Square]
+
+# Regression test for https://github.com/psf/black/issues/3756.
+[
+    (  # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+    ),
+]
+[
+    (  # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"  # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+    ),
+]
diff --git a/.vim/bundle/black/tests/data/cases/preview_context_managers_38.py b/.vim/bundle/black/tests/data/cases/preview_context_managers_38.py
new file mode 100644 (file)
index 0000000..719d94f
--- /dev/null
@@ -0,0 +1,55 @@
+# flags: --preview --minimum-version=3.8
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2() as cm2, \
+     make_context_manager3() as cm3, \
+     make_context_manager4() as cm4 \
+:
+    pass
+
+
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2(), \
+     make_context_manager3() as cm3, \
+     make_context_manager4() \
+:
+    pass
+
+
+with \
+     new_new_new1() as cm1, \
+     new_new_new2() \
+:
+    pass
+
+
+with mock.patch.object(
+    self.my_runner, "first_method", autospec=True
+) as mock_run_adb, mock.patch.object(
+    self.my_runner, "second_method", autospec=True, return_value="foo"
+):
+    pass
+
+
+# output
+
+
+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
+    pass
+
+
+with make_context_manager1() as cm1, make_context_manager2(), make_context_manager3() as cm3, make_context_manager4():
+    pass
+
+
+with new_new_new1() as cm1, new_new_new2():
+    pass
+
+
+with mock.patch.object(
+    self.my_runner, "first_method", autospec=True
+) as mock_run_adb, mock.patch.object(
+    self.my_runner, "second_method", autospec=True, return_value="foo"
+):
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_context_managers_39.py b/.vim/bundle/black/tests/data/cases/preview_context_managers_39.py
new file mode 100644 (file)
index 0000000..589e00a
--- /dev/null
@@ -0,0 +1,175 @@
+# flags: --preview --minimum-version=3.9
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2() as cm2, \
+     make_context_manager3() as cm3, \
+     make_context_manager4() as cm4 \
+:
+    pass
+
+
+# Leading comment
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2(), \
+     make_context_manager3() as cm3, \
+     make_context_manager4() \
+:
+    pass
+
+
+with \
+     new_new_new1() as cm1, \
+     new_new_new2() \
+:
+    pass
+
+
+with (
+     new_new_new1() as cm1,
+     new_new_new2()
+):
+    pass
+
+
+# Leading comment.
+with (
+     # First comment.
+     new_new_new1() as cm1,
+     # Second comment.
+     new_new_new2()
+     # Last comment.
+):
+    pass
+
+
+with \
+    this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2) as cm1, \
+    this_is_a_very_long_call(looong_arg1=looong_value1, looong_arg2=looong_value2, looong_arg3=looong_value3, looong_arg4=looong_value4) as cm2 \
+:
+    pass
+
+
+with mock.patch.object(
+    self.my_runner, "first_method", autospec=True
+) as mock_run_adb, mock.patch.object(
+    self.my_runner, "second_method", autospec=True, return_value="foo"
+):
+    pass
+
+
+with xxxxxxxx.some_kind_of_method(
+    some_argument=[
+        "first",
+        "second",
+        "third",
+    ]
+).another_method() as cmd:
+    pass
+
+
+async def func():
+    async with \
+        make_context_manager1() as cm1, \
+        make_context_manager2() as cm2, \
+        make_context_manager3() as cm3, \
+        make_context_manager4() as cm4 \
+    :
+        pass
+
+    async with some_function(
+        argument1, argument2, argument3="some_value"
+    ) as some_cm, some_other_function(
+        argument1, argument2, argument3="some_value"
+    ):
+        pass
+
+
+# output
+
+
+with (
+    make_context_manager1() as cm1,
+    make_context_manager2() as cm2,
+    make_context_manager3() as cm3,
+    make_context_manager4() as cm4,
+):
+    pass
+
+
+# Leading comment
+with (
+    make_context_manager1() as cm1,
+    make_context_manager2(),
+    make_context_manager3() as cm3,
+    make_context_manager4(),
+):
+    pass
+
+
+with new_new_new1() as cm1, new_new_new2():
+    pass
+
+
+with new_new_new1() as cm1, new_new_new2():
+    pass
+
+
+# Leading comment.
+with (
+    # First comment.
+    new_new_new1() as cm1,
+    # Second comment.
+    new_new_new2(),
+    # Last comment.
+):
+    pass
+
+
+with (
+    this_is_a_very_long_call(
+        looong_arg1=looong_value1, looong_arg2=looong_value2
+    ) as cm1,
+    this_is_a_very_long_call(
+        looong_arg1=looong_value1,
+        looong_arg2=looong_value2,
+        looong_arg3=looong_value3,
+        looong_arg4=looong_value4,
+    ) as cm2,
+):
+    pass
+
+
+with (
+    mock.patch.object(self.my_runner, "first_method", autospec=True) as mock_run_adb,
+    mock.patch.object(
+        self.my_runner, "second_method", autospec=True, return_value="foo"
+    ),
+):
+    pass
+
+
+with xxxxxxxx.some_kind_of_method(
+    some_argument=[
+        "first",
+        "second",
+        "third",
+    ]
+).another_method() as cmd:
+    pass
+
+
+async def func():
+    async with (
+        make_context_manager1() as cm1,
+        make_context_manager2() as cm2,
+        make_context_manager3() as cm3,
+        make_context_manager4() as cm4,
+    ):
+        pass
+
+    async with (
+        some_function(argument1, argument2, argument3="some_value") as some_cm,
+        some_other_function(argument1, argument2, argument3="some_value"),
+    ):
+        pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_310.py b/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_310.py
new file mode 100644 (file)
index 0000000..a9e3107
--- /dev/null
@@ -0,0 +1,36 @@
+# flags: --preview --minimum-version=3.10
+# This file uses pattern matching introduced in Python 3.10.
+
+
+match http_code:
+    case 404:
+        print("Not found")
+
+
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2() as cm2, \
+     make_context_manager3() as cm3, \
+     make_context_manager4() as cm4 \
+:
+    pass
+
+
+# output
+
+
+# This file uses pattern matching introduced in Python 3.10.
+
+
+match http_code:
+    case 404:
+        print("Not found")
+
+
+with (
+    make_context_manager1() as cm1,
+    make_context_manager2() as cm2,
+    make_context_manager3() as cm3,
+    make_context_manager4() as cm4,
+):
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_311.py b/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_311.py
new file mode 100644 (file)
index 0000000..af1e83f
--- /dev/null
@@ -0,0 +1,38 @@
+# flags: --preview --minimum-version=3.11
+# This file uses except* clause in Python 3.11.
+
+
+try:
+    some_call()
+except* Error as e:
+    pass
+
+
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2() as cm2, \
+     make_context_manager3() as cm3, \
+     make_context_manager4() as cm4 \
+:
+    pass
+
+
+# output
+
+
+# This file uses except* clause in Python 3.11.
+
+
+try:
+    some_call()
+except* Error as e:
+    pass
+
+
+with (
+    make_context_manager1() as cm1,
+    make_context_manager2() as cm2,
+    make_context_manager3() as cm3,
+    make_context_manager4() as cm4,
+):
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_38.py b/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_38.py
new file mode 100644 (file)
index 0000000..25217a4
--- /dev/null
@@ -0,0 +1,47 @@
+# flags: --preview
+# This file doesn't use any Python 3.9+ only grammars.
+
+
+# Make sure parens around a single context manager don't get autodetected as
+# Python 3.9+.
+with (a):
+    pass
+
+
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2() as cm2, \
+     make_context_manager3() as cm3, \
+     make_context_manager4() as cm4 \
+:
+    pass
+
+
+with mock.patch.object(
+    self.my_runner, "first_method", autospec=True
+) as mock_run_adb, mock.patch.object(
+    self.my_runner, "second_method", autospec=True, return_value="foo"
+):
+    pass
+
+
+# output
+# This file doesn't use any Python 3.9+ only grammars.
+
+
+# Make sure parens around a single context manager don't get autodetected as
+# Python 3.9+.
+with a:
+    pass
+
+
+with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:
+    pass
+
+
+with mock.patch.object(
+    self.my_runner, "first_method", autospec=True
+) as mock_run_adb, mock.patch.object(
+    self.my_runner, "second_method", autospec=True, return_value="foo"
+):
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_39.py b/.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_39.py
new file mode 100644 (file)
index 0000000..3f72e48
--- /dev/null
@@ -0,0 +1,35 @@
+# flags: --preview --minimum-version=3.9
+# This file uses parenthesized context managers introduced in Python 3.9.
+
+
+with \
+     make_context_manager1() as cm1, \
+     make_context_manager2() as cm2, \
+     make_context_manager3() as cm3, \
+     make_context_manager4() as cm4 \
+:
+    pass
+
+
+with (
+     new_new_new1() as cm1,
+     new_new_new2()
+):
+    pass
+
+
+# output
+# This file uses parenthesized context managers introduced in Python 3.9.
+
+
+with (
+    make_context_manager1() as cm1,
+    make_context_manager2() as cm2,
+    make_context_manager3() as cm3,
+    make_context_manager4() as cm4,
+):
+    pass
+
+
+with new_new_new1() as cm1, new_new_new2():
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_dummy_implementations.py b/.vim/bundle/black/tests/data/cases/preview_dummy_implementations.py
new file mode 100644 (file)
index 0000000..98b69bf
--- /dev/null
@@ -0,0 +1,100 @@
+# flags: --preview
+from typing import NoReturn, Protocol, Union, overload
+
+
+def dummy(a): ...
+def other(b): ...
+
+
+@overload
+def a(arg: int) -> int: ...
+@overload
+def a(arg: str) -> str: ...
+@overload
+def a(arg: object) -> NoReturn: ...
+def a(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg
+
+class Proto(Protocol):
+    def foo(self, a: int) -> int:
+        ...
+
+    def bar(self, b: str) -> str: ...
+    def baz(self, c: bytes) -> str:
+        ...
+
+
+def dummy_two():
+    ...
+@dummy
+def dummy_three():
+    ...
+
+def dummy_four():
+    ...
+
+@overload
+def b(arg: int) -> int: ...
+
+@overload
+def b(arg: str) -> str: ...
+@overload
+def b(arg: object) -> NoReturn: ...
+
+def b(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg
+
+# output
+
+from typing import NoReturn, Protocol, Union, overload
+
+
+def dummy(a): ...
+def other(b): ...
+
+
+@overload
+def a(arg: int) -> int: ...
+@overload
+def a(arg: str) -> str: ...
+@overload
+def a(arg: object) -> NoReturn: ...
+def a(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg
+
+
+class Proto(Protocol):
+    def foo(self, a: int) -> int: ...
+
+    def bar(self, b: str) -> str: ...
+    def baz(self, c: bytes) -> str: ...
+
+
+def dummy_two(): ...
+@dummy
+def dummy_three(): ...
+
+
+def dummy_four(): ...
+
+
+@overload
+def b(arg: int) -> int: ...
+
+
+@overload
+def b(arg: str) -> str: ...
+@overload
+def b(arg: object) -> NoReturn: ...
+
+
+def b(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg
diff --git a/.vim/bundle/black/tests/data/cases/preview_format_unicode_escape_seq.py b/.vim/bundle/black/tests/data/cases/preview_format_unicode_escape_seq.py
new file mode 100644 (file)
index 0000000..65c3d8d
--- /dev/null
@@ -0,0 +1,34 @@
+# flags: --preview
+x = "\x1F"
+x = "\\x1B"
+x = "\\\x1B"
+x = "\U0001F60E"
+x = "\u0001F60E"
+x = r"\u0001F60E"
+x = "don't format me"
+x = "\xA3"
+x = "\u2717"
+x = "\uFaCe"
+x = "\N{ox}\N{OX}"
+x = "\N{lAtIn smaLL letteR x}"
+x = "\N{CYRILLIC small LETTER BYELORUSSIAN-UKRAINIAN I}"
+x = b"\x1Fdon't byte"
+x = rb"\x1Fdon't format"
+
+# output
+
+x = "\x1f"
+x = "\\x1B"
+x = "\\\x1b"
+x = "\U0001f60e"
+x = "\u0001F60E"
+x = r"\u0001F60E"
+x = "don't format me"
+x = "\xa3"
+x = "\u2717"
+x = "\uface"
+x = "\N{OX}\N{OX}"
+x = "\N{LATIN SMALL LETTER X}"
+x = "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}"
+x = b"\x1fdon't byte"
+x = rb"\x1Fdon't format"
diff --git a/.vim/bundle/black/tests/data/cases/preview_long_dict_values.py b/.vim/bundle/black/tests/data/cases/preview_long_dict_values.py
new file mode 100644 (file)
index 0000000..fbbacd1
--- /dev/null
@@ -0,0 +1,91 @@
+# flags: --preview
+my_dict = {
+    "something_something":
+        r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
+        r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
+        r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
+}
+
+my_dict = {
+    "a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0
+}
+
+my_dict = {
+    "a key in my dict": a_very_long_variable * and_a_very_long_function_call() * and_another_long_func() / 100000.0
+}
+
+my_dict = {
+    "a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value")
+}
+
+{
+    'xxxxxx':
+        xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx(
+            xxxxxxxxxxxxxx={
+                'x':
+                    xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+                        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=(
+                            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+                            .xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+                                xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+                                .xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+                                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={
+                                        'x': x.xx,
+                                        'x': x.x,
+                                    }))))
+            }),
+}
+
+
+# output
+
+
+my_dict = {
+    "something_something": (
+        r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
+        r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
+        r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
+    ),
+}
+
+my_dict = {
+    "a key in my dict": (
+        a_very_long_variable * and_a_very_long_function_call() / 100000.0
+    )
+}
+
+my_dict = {
+    "a key in my dict": (
+        a_very_long_variable
+        * and_a_very_long_function_call()
+        * and_another_long_func()
+        / 100000.0
+    )
+}
+
+my_dict = {
+    "a key in my dict": (
+        MyClass.some_attribute.first_call()
+        .second_call()
+        .third_call(some_args="some value")
+    )
+}
+
+{
+    "xxxxxx": xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxx(
+        xxxxxxxxxxxxxx={
+            "x": xxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+                xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=(
+                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+                        xxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+                            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx={
+                                "x": x.xx,
+                                "x": x.x,
+                            }
+                        )
+                    )
+                )
+            )
+        }
+    ),
+}
similarity index 76%
rename from .vim/bundle/black/tests/data/long_strings.py
rename to .vim/bundle/black/tests/data/cases/preview_long_strings.py
index 430f760cf0b9dc9a8490974198904b4ff01195ba..5519f0987741967c27c60adc231c018f1b6cfbac 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --preview
 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."
@@ -18,6 +19,26 @@ D3 = {x: "This is a really long string that can't possibly be expected to fit al
 
 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)}
 
+D5 = {  # Test for https://github.com/psf/black/issues/3261
+    ("This is a really long string that can't be expected to fit in one line and is used as a nested dict's key"): {"inner": "value"},
+}
+
+D6 = {  # Test for https://github.com/psf/black/issues/3261
+    ("This is a really long string that can't be expected to fit in one line and is used as a dict's key"): ["value1", "value2"],
+}
+
+L1 = ["The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a list literal, so it's expected to be wrapped in parens when splitting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "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 list literal.", ("parens should be stripped for short string in list")]
+
+L2 = ["This is a really long string that can't be expected to fit in one line and is the only child of a list literal."]
+
+S1 = {"The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a set literal, so it's expected to be wrapped in parens when splitting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "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 set literal.", ("parens should be stripped for short string in set")}
+
+S2 = {"This is a really long string that can't be expected to fit in one line and is the only child of a set literal."}
+
+T1 = ("The is a short string", "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a tuple literal, so it's expected to be wrapped in parens when splitting to avoid implicit str concatenation.", short_call("arg", {"key": "value"}), "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 tuple literal.", ("parens should be stripped for short string in list"))
+
+T2 = ("This is a really long string that can't be expected to fit in one line and is the only child of a tuple literal.",)
+
 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 = (
@@ -72,6 +93,25 @@ bad_split_func3(
     zzz,
 )
 
+inline_comments_func1(
+    "if there are inline "
+    "comments in the middle "
+    # Here is the standard alone comment.
+    "of the implicitly concatenated "
+    "string, we should handle "
+    "them correctly",
+    xxx,
+)
+
+inline_comments_func2(
+    "what if the string is very very very very very very very very very very long and this part does "
+    "not fit into a single line? "
+    # Here is the standard alone comment.
+    "then the string should still be properly handled by merging and splitting "
+    "it into parts that fit in line length.",
+    xxx,
+)
+
 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")
@@ -90,7 +130,7 @@ fstring_with_no_fexprs = f"Some regular string that needs to get split certainly
 
 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_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 gets thrown to the top.
     "Arg #2", "Arg #3", "Arg #4", "Arg #5")
 
 pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# <pragma>: <...>` should be left alone."  # noqa: E501
@@ -239,6 +279,32 @@ string_with_escaped_nameescape = (
     "........................................................................... \\N{LAO KO LA}"
 )
 
+msg = lambda x: f"this is a very very very long lambda value {x} that doesn't fit on a single line"
+
+dict_with_lambda_values = {
+    "join": lambda j: (
+        f"{j.__class__.__name__}({some_function_call(j.left)}, "
+        f"{some_function_call(j.right)})"
+    ),
+}
+
+# Complex string concatenations with a method call in the middle.
+code = (
+    ("    return [\n")
+    + (
+        ", \n".join(
+            "        (%r, self.%s, visitor.%s)"
+            % (attrname, attrname, visit_name)
+            for attrname, visit_name in names
+        )
+    )
+    + ("\n    ]\n")
+)
+
+
+# Test case of an outer string' parens enclose an inner string's parens.
+call(body=("%s %s" % ((",".join(items)), suffix)))
+
 
 # output
 
@@ -321,11 +387,88 @@ D4 = {
     "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),
+        " dictionary. It is %s bad (#%d)." % ("soooo", 2)
+    ),
+}
+
+D5 = {  # Test for https://github.com/psf/black/issues/3261
+    "This is a really long string that can't be expected to fit in one line and is used as a nested dict's key": {
+        "inner": "value"
+    },
+}
+
+D6 = {  # Test for https://github.com/psf/black/issues/3261
+    "This is a really long string that can't be expected to fit in one line and is used as a dict's key": [
+        "value1",
+        "value2",
+    ],
+}
+
+L1 = [
+    "The is a short string",
+    (
+        "This is a really long string that can't possibly be expected to fit all"
+        " together on one line. Also it is inside a list literal, so it's expected to"
+        " be wrapped in parens when splitting to avoid implicit str concatenation."
+    ),
+    short_call("arg", {"key": "value"}),
+    (
+        "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 list"
+        " literal."
+    ),
+    "parens should be stripped for short string in list",
+]
+
+L2 = [
+    "This is a really long string that can't be expected to fit in one line and is the"
+    " only child of a list literal."
+]
+
+S1 = {
+    "The is a short string",
+    (
+        "This is a really long string that can't possibly be expected to fit all"
+        " together on one line. Also it is inside a set literal, so it's expected to be"
+        " wrapped in parens when splitting to avoid implicit str concatenation."
+    ),
+    short_call("arg", {"key": "value"}),
+    (
+        "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 set"
+        " literal."
+    ),
+    "parens should be stripped for short string in set",
+}
+
+S2 = {
+    "This is a really long string that can't be expected to fit in one line and is the"
+    " only child of a set literal."
 }
 
+T1 = (
+    "The is a short string",
+    (
+        "This is a really long string that can't possibly be expected to fit all"
+        " together on one line. Also it is inside a tuple literal, so it's expected to"
+        " be wrapped in parens when splitting to avoid implicit str concatenation."
+    ),
+    short_call("arg", {"key": "value"}),
+    (
+        "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 tuple"
+        " literal."
+    ),
+    "parens should be stripped for short string in list",
+)
+
+T2 = (
+    (
+        "This is a really long string that can't be expected to fit in one line and is"
+        " the only child of a tuple literal."
+    ),
+)
+
 func_with_keywords(
     my_arg,
     my_kwarg=(
@@ -395,6 +538,22 @@ bad_split_func3(
     zzz,
 )
 
+inline_comments_func1(
+    "if there are inline comments in the middle "
+    # Here is the standard alone comment.
+    "of the implicitly concatenated string, we should handle them correctly",
+    xxx,
+)
+
+inline_comments_func2(
+    "what if the string is very very very very very very very very very very long and"
+    " this part does not fit into a single line? "
+    # Here is the standard alone comment.
+    "then the string should still be properly handled by merging and splitting "
+    "it into parts that fit in line length.",
+    xxx,
+)
+
 raw_string = (
     r"This is a long raw string. When re-formatting this string, black needs to make"
     r" sure it prepends the 'r' onto the new string."
@@ -452,7 +611,7 @@ comment_string = (  # 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.
+    " enclosing left parentheses.",  # This comment gets thrown to the top.
     "Arg #2",
     "Arg #3",
     "Arg #4",
@@ -659,3 +818,31 @@ string_with_escaped_nameescape = (
     "..........................................................................."
     " \\N{LAO KO LA}"
 )
+
+msg = (
+    lambda x: (
+        f"this is a very very very long lambda value {x} that doesn't fit on a single"
+        " line"
+    )
+)
+
+dict_with_lambda_values = {
+    "join": lambda j: (
+        f"{j.__class__.__name__}({some_function_call(j.left)}, "
+        f"{some_function_call(j.right)})"
+    ),
+}
+
+# Complex string concatenations with a method call in the middle.
+code = (
+    "    return [\n"
+    + ", \n".join(
+        "        (%r, self.%s, visitor.%s)" % (attrname, attrname, visit_name)
+        for attrname, visit_name in names
+    )
+    + "\n    ]\n"
+)
+
+
+# Test case of an outer string' parens enclose an inner string's parens.
+call(body="%s %s" % (",".join(items), suffix))
diff --git a/.vim/bundle/black/tests/data/cases/preview_long_strings__east_asian_width.py b/.vim/bundle/black/tests/data/cases/preview_long_strings__east_asian_width.py
new file mode 100644 (file)
index 0000000..d190f42
--- /dev/null
@@ -0,0 +1,26 @@
+# flags: --preview\r
+# The following strings do not have not-so-many chars, but are long enough\r
+# when these are rendered in a monospace font (if the renderer respects\r
+# Unicode East Asian Width properties).\r
+hangul = '코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이 필요한 문자열'\r
+hanzi = '中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長,因此需要換行的字符串。'\r
+japanese = 'コードポイントの数は少ないが、実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、改行が要る文字列'\r
+\r
+# output\r
+\r
+# The following strings do not have not-so-many chars, but are long enough\r
+# when these are rendered in a monospace font (if the renderer respects\r
+# Unicode East Asian Width properties).\r
+hangul = (\r
+    "코드포인트 수는 적으나 실제 터미널이나 에디터에서 렌더링될 땐 너무 길어서 줄바꿈이"\r
+    " 필요한 문자열"\r
+)\r
+hanzi = (\r
+    "中文測試:代碼點數量少,但在真正的終端模擬器或編輯器中呈現時太長,"\r
+    "因此需要換行的字符串。"\r
+)\r
+japanese = (\r
+    "コードポイントの数は少ないが、"\r
+    "実際の端末エミュレータやエディタでレンダリングされる時は長すぎる為、"\r
+    "改行が要る文字列"\r
+)\r
similarity index 99%
rename from .vim/bundle/black/tests/data/long_strings__edge_case.py
rename to .vim/bundle/black/tests/data/cases/preview_long_strings__edge_case.py
index 2bc0b6ed32885a7bac4c816d6bcd3c3e4ce6fcca..a8e8971968cffff7a5e4d355bc2ef8135b92702e 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --preview
 some_variable = "This string is long but not so long that it needs to be split just yet"
 some_variable = 'This string is long but not so long that it needs to be split just yet'
 some_variable = "This string is long, just long enough that it needs to be split, u get?"
similarity index 89%
rename from .vim/bundle/black/tests/data/long_strings__regression.py
rename to .vim/bundle/black/tests/data/cases/preview_long_strings__regression.py
index 36f323e04d6de18e7ca0041200951706c001b695..436157f4e0584b949c109db62b3a0bad3fb97cf0 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --preview
 class A:
     def foo():
         result = type(message)("")
@@ -209,8 +210,8 @@ def foo():
 
 some_tuple = ("some string", "some string" " which should be joined")
 
-some_commented_string = (
-    "This string is long but not so long that it needs hahahah toooooo be so greatttt"  # This comment gets thrown to the top.
+some_commented_string = (  # This comment stays at the top.
+    "This string is long but not so long that it needs hahahah toooooo be so greatttt"
     " {} that I just can't think of any more good words to say about it at"
     " allllllllllll".format("ha")  # comments here are fine
 )
@@ -524,6 +525,42 @@ xxxxxx_xxx_xxxx_xx_xxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxx_xxxx_xxxx_xxxxx = xxxx.xxx
     },
 )
 
+# Regression test for https://github.com/psf/black/issues/3117.
+some_dict = {
+    "something_something":
+        r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
+        r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
+}
+
+# Regression test for https://github.com/psf/black/issues/3459.
+xxxx(
+    empty_str_as_first_split=''
+    f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx '
+    'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. '
+    f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}',
+    empty_u_str_as_first_split=u''
+    f'xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx '
+    'xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. '
+    f'xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}',
+)
+
+# Regression test for https://github.com/psf/black/issues/3455.
+a_dict = {
+    "/this/is/a/very/very/very/very/very/very/very/very/very/very/long/key/without/spaces":
+        # And there is a comment before the value
+        ("item1", "item2", "item3"),
+}
+
+# Regression test for https://github.com/psf/black/issues/3506.
+s = (
+    "With single quote: ' "
+    f" {my_dict['foo']}"
+    ' With double quote: " '
+    f' {my_dict["bar"]}'
+)
+
+s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:\'{my_dict["foo"]}\''
+
 
 # output
 
@@ -599,7 +636,7 @@ class A:
 
 
 def foo(xxxx):
-    for (xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx) in xxxx:
+    for xxx_xxxx, _xxx_xxx, _xxx_xxxxx, xxx_xxxx in xxxx:
         for xxx in xxx_xxxx:
             assert ("x" in xxx) or (xxx in xxx_xxx_xxxxx), (
                 "{0} xxxxxxx xx {1}, xxx {1} xx xxx xx xxxx xx xxx xxxx: xxx xxxx {2}"
@@ -763,20 +800,28 @@ class A:
 
 some_dictionary = {
     "xxxxx006": [
-        "xxx-xxx"
-        " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx=="
-        " xxxxx000 xxxxxxxxxx\n",
-        "xxx-xxx"
-        " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx=="
-        " xxxxx010 xxxxxxxxxx\n",
+        (
+            "xxx-xxx"
+            " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx=="
+            " xxxxx000 xxxxxxxxxx\n"
+        ),
+        (
+            "xxx-xxx"
+            " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx=="
+            " xxxxx010 xxxxxxxxxx\n"
+        ),
     ],
     "xxxxx016": [
-        "xxx-xxx"
-        " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx=="
-        " xxxxx000 xxxxxxxxxx\n",
-        "xxx-xxx"
-        " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx=="
-        " xxxxx010 xxxxxxxxxx\n",
+        (
+            "xxx-xxx"
+            " xxxxx3xxxx1xx2xxxxxxxxxxxxxx0xx6xxxxxxxxxx2xxxxxx9xxxxxxxxxx0xxxxx1xxx2x/xx9xx6+x+xxxxxxxxxxxxxx4xxxxxxxxxxxxxxxxxxxxx43xxx2xx2x4x++xxx6xxxxxxxxx+xxxxx/xx9x+xxxxxxxxxxxxxx8x15xxxxxxxxxxxxxxxxx82xx/xxxxxxxxxxxxxx/x5xxxxxxxxxxxxxx6xxxxxx74x4/xxx4x+xxxxxxxxx2xxxxxxxx87xxxxx4xxxxxxxx3xx0xxxxx4xxx1xx9xx5xxxxxxx/xxxxx5xx6xx4xxxx1x/x2xxxxxxxxxxxx64xxxxxxx1x0xx5xxxxxxxxxxxxxx=="
+            " xxxxx000 xxxxxxxxxx\n"
+        ),
+        (
+            "xxx-xxx"
+            " xxxxx3xxxx1xx2xxxxxxxxxxxxxx6xxxxxxxxxxxxxx9xxxxxxxxxxxxx3xxx9xxxxxxxxxxxxxxxx0xxxxxxxxxxxxxxxxx2xxxx2xxx6xxxxx/xx54xxxxxxxxx4xxx3xxxxxx9xx3xxxxx39xxxxxxxxx5xx91xxxx7xxxxxx8xxxxxxxxxxxxxxxx9xxx93xxxxxxxxxxxxxxxxx7xxx8xx8xx4/x1xxxxx1x3xxxxxxxxxxxxx3xxxxxx9xx4xx4x7xxxxxxxxxxxxx1xxxxxxxxx7xxxxxxxxxxxxxx4xx6xxxxxxxxx9xxx7xxxx2xxxxxxxxxxxxxxxxxxxxxx8xxxxxxxxxxxxxxxxxxxx6xx=="
+            " xxxxx010 xxxxxxxxxx\n"
+        ),
     ],
 }
 
@@ -789,7 +834,7 @@ def foo():
 
 some_tuple = ("some string", "some string which should be joined")
 
-some_commented_string = (  # This comment gets thrown to the top.
+some_commented_string = (  # This comment stays at the top.
     "This string is long but not so long that it needs hahahah toooooo be so greatttt"
     " {} that I just can't think of any more good words to say about it at"
     " allllllllllll".format("ha")  # comments here are fine
@@ -962,9 +1007,9 @@ class xxxxxxxxxxxxxxxxxxxxx(xxxx.xxxxxxxxxxxxx):
         )
 
 
-value.__dict__[
-    key
-] = "test"  # set some Thrift field to non-None in the struct aa bb cc dd ee
+value.__dict__[key] = (
+    "test"  # set some Thrift field to non-None in the struct aa bb cc dd ee
+)
 
 RE_ONE_BACKSLASH = {
     "asdf_hjkl_jkl": re.compile(
@@ -1164,3 +1209,42 @@ xxxxxx_xxx_xxxx_xx_xxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxx_xxxx_xxxx_xxxxx = xxxx.xxx
         ),
     },
 )
+
+# Regression test for https://github.com/psf/black/issues/3117.
+some_dict = {
+    "something_something": (
+        r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
+        r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
+    ),
+}
+
+# Regression test for https://github.com/psf/black/issues/3459.
+xxxx(
+    empty_str_as_first_split=(
+        ""
+        f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx "
+        "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. "
+        f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}"
+    ),
+    empty_u_str_as_first_split=(
+        ""
+        f"xxxxxxx {xxxxxxxxxx} xxx xxxxxxxxxx xxxxx xxx xxx xx "
+        "xxxxx xxxxxxxxx xxxxxxx, xxx xxxxxxxxxxx xxx xxxxx. "
+        f"xxxxxxxxxxxxx xxxx xx xxxxxxxxxx. xxxxx: {x.xxx}"
+    ),
+)
+
+# Regression test for https://github.com/psf/black/issues/3455.
+a_dict = {
+    "/this/is/a/very/very/very/very/very/very/very/very/very/very/long/key/without/spaces":
+    # And there is a comment before the value
+    ("item1", "item2", "item3"),
+}
+
+# Regression test for https://github.com/psf/black/issues/3506.
+s = f"With single quote: '  {my_dict['foo']} With double quote: \"  {my_dict['bar']}"
+
+s = (
+    "Lorem Ipsum is simply dummy text of the printing and typesetting"
+    f" industry:'{my_dict['foo']}'"
+)
diff --git a/.vim/bundle/black/tests/data/cases/preview_long_strings__type_annotations.py b/.vim/bundle/black/tests/data/cases/preview_long_strings__type_annotations.py
new file mode 100644 (file)
index 0000000..8beb877
--- /dev/null
@@ -0,0 +1,60 @@
+# flags: --preview
+def func(
+    arg1,
+    arg2,
+) -> Set["this_is_a_very_long_module_name.AndAVeryLongClasName"
+         ".WithAVeryVeryVeryVeryVeryLongSubClassName"]:
+  pass
+
+
+def func(
+    argument: (
+        "VeryLongClassNameWithAwkwardGenericSubtype[int] |"
+        "VeryLongClassNameWithAwkwardGenericSubtype[str]"
+    ),
+) -> (
+    "VeryLongClassNameWithAwkwardGenericSubtype[int] |"
+    "VeryLongClassNameWithAwkwardGenericSubtype[str]"
+):
+  pass
+
+
+def func(
+    argument: (
+        "int |"
+        "str"
+    ),
+) -> Set["int |"
+         " str"]:
+  pass
+
+
+# output
+
+
+def func(
+    arg1,
+    arg2,
+) -> Set[
+    "this_is_a_very_long_module_name.AndAVeryLongClasName"
+    ".WithAVeryVeryVeryVeryVeryLongSubClassName"
+]:
+    pass
+
+
+def func(
+    argument: (
+        "VeryLongClassNameWithAwkwardGenericSubtype[int] |"
+        "VeryLongClassNameWithAwkwardGenericSubtype[str]"
+    ),
+) -> (
+    "VeryLongClassNameWithAwkwardGenericSubtype[int] |"
+    "VeryLongClassNameWithAwkwardGenericSubtype[str]"
+):
+    pass
+
+
+def func(
+    argument: "int |" "str",
+) -> Set["int |" " str"]:
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_multiline_strings.py b/.vim/bundle/black/tests/data/cases/preview_multiline_strings.py
new file mode 100644 (file)
index 0000000..3ff6436
--- /dev/null
@@ -0,0 +1,387 @@
+# flags: --preview
+"""cow
+say""",
+call(3, "dogsay", textwrap.dedent("""dove
+    coo""" % "cowabunga"))
+call(3, "dogsay", textwrap.dedent("""dove
+coo""" % "cowabunga"))
+call(3, textwrap.dedent("""cow
+    moo""" % "cowabunga"), "dogsay")
+call(3, "dogsay", textwrap.dedent("""crow
+    caw""" % "cowabunga"),)
+call(3, textwrap.dedent("""cat
+    meow""" % "cowabunga"), {"dog", "say"})
+call(3, {"dog", "say"}, textwrap.dedent("""horse
+    neigh""" % "cowabunga"))
+call(3, {"dog", "say"}, textwrap.dedent("""pig
+    oink""" % "cowabunga"),)
+textwrap.dedent("""A one-line triple-quoted string.""")
+textwrap.dedent("""A two-line triple-quoted string
+since it goes to the next line.""")
+textwrap.dedent("""A three-line triple-quoted string
+that not only goes to the next line
+but also goes one line beyond.""")
+textwrap.dedent("""\
+    A triple-quoted string
+    actually leveraging the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. file contents.
+""")
+path.write_text(textwrap.dedent("""\
+    A triple-quoted string
+    actually leveraging the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. file contents.
+"""))
+path.write_text(textwrap.dedent("""\
+    A triple-quoted string
+    actually leveraging the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. {config_filename} file contents.
+""".format("config_filename", config_filename)))
+# Another use case
+data = yaml.load("""\
+a: 1
+b: 2
+""")
+data = yaml.load("""\
+a: 1
+b: 2
+""",)
+data = yaml.load(
+    """\
+    a: 1
+    b: 2
+"""
+)
+
+MULTILINE = """
+foo
+""".replace("\n", "")
+generated_readme = lambda project_name: """
+{}
+
+<Add content here!>
+""".strip().format(project_name)
+parser.usage += """
+Custom extra help summary.
+
+Extra test:
+- with
+- bullets
+"""
+
+
+def get_stuff(cr, value):
+    # original
+    cr.execute("""
+        SELECT whatever
+          FROM some_table t
+         WHERE id = %s
+    """, [value])
+    return cr.fetchone()
+
+
+def get_stuff(cr, value):
+    # preferred
+    cr.execute(
+        """
+        SELECT whatever
+          FROM some_table t
+         WHERE id = %s
+        """,
+        [value],
+    )
+    return cr.fetchone()
+
+
+call(arg1, arg2, """
+short
+""", arg3=True)
+test_vectors = [
+    "one-liner\n",
+    "two\nliner\n",
+    """expressed
+as a three line
+mulitline string""",
+]
+
+_wat = re.compile(
+    r"""
+    regex
+    """,
+    re.MULTILINE | re.VERBOSE,
+)
+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,)
+path.write_text(textwrap.dedent("""\
+    A triple-quoted string
+    actually {verb} the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. {file_type} file contents.
+""".format(verb="using", file_type="test")))
+{"""cow
+moos"""}
+["""cow
+moos"""]
+["""cow
+moos""", """dog
+woofs
+and
+barks"""]
+def dastardly_default_value(
+    cow: String = json.loads("""this
+is
+quite
+the
+dastadardly
+value!"""),
+    **kwargs,
+):
+    pass
+
+print(f"""
+    This {animal}
+    moos and barks
+{animal} say
+""")
+msg = f"""The arguments {bad_arguments} were passed in.
+Please use `--build-option` instead,
+`--global-option` is reserved to flags like `--verbose` or `--quiet`.
+"""
+
+this_will_become_one_line = (
+    "a"
+    "b"
+    "c"
+)
+
+this_will_stay_on_three_lines = (
+    "a"  # comment
+    "b"
+    "c"
+)
+
+this_will_also_become_one_line = (  # comment
+    "a"
+    "b"
+    "c"
+)
+
+# output
+"""cow
+say""",
+call(
+    3,
+    "dogsay",
+    textwrap.dedent("""dove
+    coo""" % "cowabunga"),
+)
+call(
+    3,
+    "dogsay",
+    textwrap.dedent("""dove
+coo""" % "cowabunga"),
+)
+call(
+    3,
+    textwrap.dedent("""cow
+    moo""" % "cowabunga"),
+    "dogsay",
+)
+call(
+    3,
+    "dogsay",
+    textwrap.dedent("""crow
+    caw""" % "cowabunga"),
+)
+call(
+    3,
+    textwrap.dedent("""cat
+    meow""" % "cowabunga"),
+    {"dog", "say"},
+)
+call(
+    3,
+    {"dog", "say"},
+    textwrap.dedent("""horse
+    neigh""" % "cowabunga"),
+)
+call(
+    3,
+    {"dog", "say"},
+    textwrap.dedent("""pig
+    oink""" % "cowabunga"),
+)
+textwrap.dedent("""A one-line triple-quoted string.""")
+textwrap.dedent("""A two-line triple-quoted string
+since it goes to the next line.""")
+textwrap.dedent("""A three-line triple-quoted string
+that not only goes to the next line
+but also goes one line beyond.""")
+textwrap.dedent("""\
+    A triple-quoted string
+    actually leveraging the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. file contents.
+""")
+path.write_text(textwrap.dedent("""\
+    A triple-quoted string
+    actually leveraging the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. file contents.
+"""))
+path.write_text(textwrap.dedent("""\
+    A triple-quoted string
+    actually leveraging the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. {config_filename} file contents.
+""".format("config_filename", config_filename)))
+# Another use case
+data = yaml.load("""\
+a: 1
+b: 2
+""")
+data = yaml.load(
+    """\
+a: 1
+b: 2
+""",
+)
+data = yaml.load("""\
+    a: 1
+    b: 2
+""")
+
+MULTILINE = """
+foo
+""".replace("\n", "")
+generated_readme = lambda project_name: """
+{}
+
+<Add content here!>
+""".strip().format(project_name)
+parser.usage += """
+Custom extra help summary.
+
+Extra test:
+- with
+- bullets
+"""
+
+
+def get_stuff(cr, value):
+    # original
+    cr.execute(
+        """
+        SELECT whatever
+          FROM some_table t
+         WHERE id = %s
+    """,
+        [value],
+    )
+    return cr.fetchone()
+
+
+def get_stuff(cr, value):
+    # preferred
+    cr.execute(
+        """
+        SELECT whatever
+          FROM some_table t
+         WHERE id = %s
+        """,
+        [value],
+    )
+    return cr.fetchone()
+
+
+call(
+    arg1,
+    arg2,
+    """
+short
+""",
+    arg3=True,
+)
+test_vectors = [
+    "one-liner\n",
+    "two\nliner\n",
+    """expressed
+as a three line
+mulitline string""",
+]
+
+_wat = re.compile(
+    r"""
+    regex
+    """,
+    re.MULTILINE | re.VERBOSE,
+)
+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,)
+path.write_text(textwrap.dedent("""\
+    A triple-quoted string
+    actually {verb} the textwrap.dedent functionality
+    that ends in a trailing newline,
+    representing e.g. {file_type} file contents.
+""".format(verb="using", file_type="test")))
+{"""cow
+moos"""}
+["""cow
+moos"""]
+[
+    """cow
+moos""",
+    """dog
+woofs
+and
+barks""",
+]
+
+
+def dastardly_default_value(
+    cow: String = json.loads("""this
+is
+quite
+the
+dastadardly
+value!"""),
+    **kwargs,
+):
+    pass
+
+
+print(f"""
+    This {animal}
+    moos and barks
+{animal} say
+""")
+msg = f"""The arguments {bad_arguments} were passed in.
+Please use `--build-option` instead,
+`--global-option` is reserved to flags like `--verbose` or `--quiet`.
+"""
+
+this_will_become_one_line = "abc"
+
+this_will_stay_on_three_lines = (
+    "a"  # comment
+    "b"
+    "c"
+)
+
+this_will_also_become_one_line = "abc"  # comment
diff --git a/.vim/bundle/black/tests/data/cases/preview_no_blank_line_before_docstring.py b/.vim/bundle/black/tests/data/cases/preview_no_blank_line_before_docstring.py
new file mode 100644 (file)
index 0000000..303035a
--- /dev/null
@@ -0,0 +1,59 @@
+# flags: --preview
+def line_before_docstring():
+
+    """Please move me up"""
+
+
+class LineBeforeDocstring:
+
+    """Please move me up"""
+
+
+class EvenIfThereIsAMethodAfter:
+
+    """I'm the docstring"""
+    def method(self):
+        pass
+
+
+class TwoLinesBeforeDocstring:
+
+
+    """I want to be treated the same as if I were closer"""
+
+
+class MultilineDocstringsAsWell:
+
+    """I'm so far
+
+    and on so many lines...
+    """
+
+
+# output
+
+
+def line_before_docstring():
+    """Please move me up"""
+
+
+class LineBeforeDocstring:
+    """Please move me up"""
+
+
+class EvenIfThereIsAMethodAfter:
+    """I'm the docstring"""
+
+    def method(self):
+        pass
+
+
+class TwoLinesBeforeDocstring:
+    """I want to be treated the same as if I were closer"""
+
+
+class MultilineDocstringsAsWell:
+    """I'm so far
+
+    and on so many lines...
+    """
diff --git a/.vim/bundle/black/tests/data/cases/preview_pep_572.py b/.vim/bundle/black/tests/data/cases/preview_pep_572.py
new file mode 100644 (file)
index 0000000..8e801ff
--- /dev/null
@@ -0,0 +1,7 @@
+# flags: --preview
+x[(a:=0):]
+x[:(a:=0)]
+
+# output
+x[(a := 0):]
+x[:(a := 0)]
similarity index 96%
rename from .vim/bundle/black/tests/data/percent_precedence.py
rename to .vim/bundle/black/tests/data/cases/preview_percent_precedence.py
index b895443fb46bbf372727bef666a1c3e23deaa536..aeaf450ff5ea4b419654a9983bc196153fe5c6d7 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --preview
 ("" % a) ** 2
 ("" % a)[0]
 ("" % a)()
diff --git a/.vim/bundle/black/tests/data/cases/preview_power_op_spacing.py b/.vim/bundle/black/tests/data/cases/preview_power_op_spacing.py
new file mode 100644 (file)
index 0000000..650c6fe
--- /dev/null
@@ -0,0 +1,97 @@
+# flags: --preview
+a = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1
+b = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1
+c = 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1 ** 1
+d = 1**1 ** 1**1 ** 1**1 ** 1**1 ** 1**1**1 ** 1 ** 1**1 ** 1**1**1**1**1 ** 1 ** 1**1**1 **1**1** 1 ** 1 ** 1
+e = 𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟
+f = 𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟
+
+a = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0
+b = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0
+c = 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0 ** 1.0
+d = 1.0**1.0 ** 1.0**1.0 ** 1.0**1.0 ** 1.0**1.0 ** 1.0**1.0**1.0 ** 1.0 ** 1.0**1.0 ** 1.0**1.0**1.0
+
+# output
+a = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1
+b = (
+    1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+    ** 1
+)
+c = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1
+d = 1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1**1
+e = 𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟**𨉟
+f = (
+    𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+    ** 𨉟
+)
+
+a = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0
+b = (
+    1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+    ** 1.0
+)
+c = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0
+d = 1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0**1.0
diff --git a/.vim/bundle/black/tests/data/cases/preview_prefer_rhs_split.py b/.vim/bundle/black/tests/data/cases/preview_prefer_rhs_split.py
new file mode 100644 (file)
index 0000000..c732c33
--- /dev/null
@@ -0,0 +1,86 @@
+# flags: --preview
+first_item, second_item = (
+    some_looooooooong_module.some_looooooooooooooong_function_name(
+        first_argument, second_argument, third_argument
+    )
+)
+
+some_dict["with_a_long_key"] = (
+    some_looooooooong_module.some_looooooooooooooong_function_name(
+        first_argument, second_argument, third_argument
+    )
+)
+
+# Make sure it works when the RHS only has one pair of (optional) parens.
+first_item, second_item = (
+    some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
+)
+
+some_dict["with_a_long_key"] = (
+    some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
+)
+
+# Make sure chaining assignments work.
+first_item, second_item, third_item, forth_item = m["everything"] = (
+    some_looooooooong_module.some_looooooooooooooong_function_name(
+        first_argument, second_argument, third_argument
+    )
+)
+
+# Make sure when the RHS's first split at the non-optional paren fits,
+# we split there instead of the outer RHS optional paren.
+first_item, second_item = some_looooooooong_module.some_loooooog_function_name(
+    first_argument, second_argument, third_argument
+)
+
+(
+    first_item,
+    second_item,
+    third_item,
+    forth_item,
+    fifth_item,
+    last_item_very_loooooong,
+) = some_looooooooong_module.some_looooooooooooooong_function_name(
+    first_argument, second_argument, third_argument
+)
+
+(
+    first_item,
+    second_item,
+    third_item,
+    forth_item,
+    fifth_item,
+    last_item_very_loooooong,
+) = everything = some_looooong_function_name(
+    first_argument, second_argument, third_argument
+)
+
+
+# Make sure unsplittable type ignore won't be moved.
+some_kind_of_table[some_key] = util.some_function(  # type: ignore  # noqa: E501
+    some_arg
+).intersection(pk_cols)
+
+some_kind_of_table[
+    some_key
+] = lambda obj: obj.some_long_named_method()  # type: ignore  # noqa: E501
+
+some_kind_of_table[
+    some_key  # type: ignore  # noqa: E501
+] = lambda obj: obj.some_long_named_method()
+
+
+# Make when when the left side of assignment plus the opening paren "... = (" is
+# exactly line length limit + 1, it won't be split like that.
+xxxxxxxxx_yyy_zzzzzzzz[
+    xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
+] = 1
+
+
+# Right side of assignment contains un-nested pairs of inner parens.
+some_kind_of_instance.some_kind_of_map[a_key] = (
+    isinstance(some_var, SomeClass)
+    and table.something_and_something != table.something_else
+) or (
+    isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing
+)
diff --git a/.vim/bundle/black/tests/data/cases/preview_return_annotation_brackets_string.py b/.vim/bundle/black/tests/data/cases/preview_return_annotation_brackets_string.py
new file mode 100644 (file)
index 0000000..fea0ea6
--- /dev/null
@@ -0,0 +1,24 @@
+# flags: --preview
+# Long string example
+def frobnicate() -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]":
+    pass
+
+# splitting the string breaks if there's any parameters
+def frobnicate(a) -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]":
+    pass
+
+# output
+
+# Long string example
+def frobnicate() -> (
+    "ThisIsTrulyUnreasonablyExtremelyLongClassName |"
+    " list[ThisIsTrulyUnreasonablyExtremelyLongClassName]"
+):
+    pass
+
+
+# splitting the string breaks if there's any parameters
+def frobnicate(
+    a,
+) -> "ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]":
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/preview_trailing_comma.py b/.vim/bundle/black/tests/data/cases/preview_trailing_comma.py
new file mode 100644 (file)
index 0000000..bba7e7a
--- /dev/null
@@ -0,0 +1,56 @@
+# flags: --preview
+e = {
+    "a": fun(msg, "ts"),
+    "longggggggggggggggid": ...,
+    "longgggggggggggggggggggkey": ..., "created": ...
+    # "longkey": ...
+}
+f = [
+    arg1,
+    arg2,
+    arg3, arg4
+    # comment
+]
+g = (
+    arg1,
+    arg2,
+    arg3, arg4
+    # comment
+)
+h = {
+    arg1,
+    arg2,
+    arg3, arg4
+    # comment
+}
+
+# output
+
+e = {
+    "a": fun(msg, "ts"),
+    "longggggggggggggggid": ...,
+    "longgggggggggggggggggggkey": ...,
+    "created": ...,
+    # "longkey": ...
+}
+f = [
+    arg1,
+    arg2,
+    arg3,
+    arg4,
+    # comment
+]
+g = (
+    arg1,
+    arg2,
+    arg3,
+    arg4,
+    # comment
+)
+h = {
+    arg1,
+    arg2,
+    arg3,
+    arg4,
+    # comment
+}
diff --git a/.vim/bundle/black/tests/data/cases/py310_pep572.py b/.vim/bundle/black/tests/data/cases/py310_pep572.py
new file mode 100644 (file)
index 0000000..172be38
--- /dev/null
@@ -0,0 +1,13 @@
+# flags: --preview --minimum-version=3.10
+x[a:=0]
+x[a := 0]
+x[a := 0, b := 1]
+x[5, b := 0]
+x[a:=0,b:=1]
+
+# output
+x[a := 0]
+x[a := 0]
+x[a := 0, b := 1]
+x[5, b := 0]
+x[a := 0, b := 1]
similarity index 95%
rename from .vim/bundle/black/tests/data/python37.py
rename to .vim/bundle/black/tests/data/cases/python37.py
index dab8b404a739c57f5258694e28ebe65bd9714462..3f61106c45dc96a8731bac308b0cdb5676ffb8a4 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3.7
+# flags: --minimum-version=3.7
 
 
 def f():
@@ -33,9 +33,6 @@ def make_arange(n):
 # output
 
 
-#!/usr/bin/env python3.7
-
-
 def f():
     return (i * 2 async for i in arange(42))
 
similarity index 93%
rename from .vim/bundle/black/tests/data/python38.py
rename to .vim/bundle/black/tests/data/cases/python38.py
index 63b0588bc27a1affe2d5a955537728ad8f95f5be..919ea6aeed475f0bc27ba47e90c8e1d94f0f314b 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3.8
+# flags: --minimum-version=3.8
 
 
 def starred_return():
@@ -22,9 +22,6 @@ def t():
 # output
 
 
-#!/usr/bin/env python3.8
-
-
 def starred_return():
     my_list = ["value2", "value3"]
     return "value1", *my_list
similarity index 91%
rename from .vim/bundle/black/tests/data/python39.py
rename to .vim/bundle/black/tests/data/cases/python39.py
index ae67c2257ebba5802cad6b259ca798574dd15515..1b9536c1529314c5ac26fd5ed3af1b8d98f658ea 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3.9
+# flags: --minimum-version=3.9
 
 @relaxed_decorator[0]
 def f():
@@ -14,10 +14,6 @@ def f():
 
 # output
 
-
-#!/usr/bin/env python3.9
-
-
 @relaxed_decorator[0]
 def f():
     ...
diff --git a/.vim/bundle/black/tests/data/cases/remove_await_parens.py b/.vim/bundle/black/tests/data/cases/remove_await_parens.py
new file mode 100644 (file)
index 0000000..8c7223d
--- /dev/null
@@ -0,0 +1,176 @@
+import asyncio
+
+# Control example
+async def main():
+    await asyncio.sleep(1)
+
+# Remove brackets for short coroutine/task
+async def main():
+    await (asyncio.sleep(1))
+
+async def main():
+    await (
+        asyncio.sleep(1)
+    )
+
+async def main():
+    await (asyncio.sleep(1)
+    )
+
+# Check comments
+async def main():
+    await (  # Hello
+        asyncio.sleep(1)
+    )
+
+async def main():
+    await (
+        asyncio.sleep(1)  # Hello
+    )
+
+async def main():
+    await (
+        asyncio.sleep(1)
+    )  # Hello
+
+# Long lines
+async def main():
+    await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1))
+
+# Same as above but with magic trailing comma in function
+async def main():
+    await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1),)
+
+# Cr@zY Br@ck3Tz
+async def main():
+    await (
+        (((((((((((((
+        (((        (((
+        (((         (((
+        (((         (((
+        (((        (((
+        ((black(1)))
+        )))        )))
+        )))         )))
+        )))         )))
+        )))        )))
+        )))))))))))))
+    )
+
+# Keep brackets around non power operations and nested awaits
+async def main():
+    await (set_of_tasks | other_set)
+
+async def main():
+    await (await asyncio.sleep(1))
+
+# It's awaits all the way down...
+async def main():
+    await (await x)
+
+async def main():
+    await (yield x)
+
+async def main():
+    await (await (asyncio.sleep(1)))
+
+async def main():
+    await (await (await (await (await (asyncio.sleep(1))))))
+
+async def main():
+    await (yield)
+
+# output
+import asyncio
+
+
+# Control example
+async def main():
+    await asyncio.sleep(1)
+
+
+# Remove brackets for short coroutine/task
+async def main():
+    await asyncio.sleep(1)
+
+
+async def main():
+    await asyncio.sleep(1)
+
+
+async def main():
+    await asyncio.sleep(1)
+
+
+# Check comments
+async def main():
+    await asyncio.sleep(1)  # Hello
+
+
+async def main():
+    await asyncio.sleep(1)  # Hello
+
+
+async def main():
+    await asyncio.sleep(1)  # Hello
+
+
+# Long lines
+async def main():
+    await asyncio.gather(
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+    )
+
+
+# Same as above but with magic trailing comma in function
+async def main():
+    await asyncio.gather(
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+        asyncio.sleep(1),
+    )
+
+
+# Cr@zY Br@ck3Tz
+async def main():
+    await black(1)
+
+
+# Keep brackets around non power operations and nested awaits
+async def main():
+    await (set_of_tasks | other_set)
+
+
+async def main():
+    await (await asyncio.sleep(1))
+
+
+# It's awaits all the way down...
+async def main():
+    await (await x)
+
+
+async def main():
+    await (yield x)
+
+
+async def main():
+    await (await asyncio.sleep(1))
+
+
+async def main():
+    await (await (await (await (await asyncio.sleep(1)))))
+
+
+async def main():
+    await (yield)
diff --git a/.vim/bundle/black/tests/data/cases/remove_except_parens.py b/.vim/bundle/black/tests/data/cases/remove_except_parens.py
new file mode 100644 (file)
index 0000000..322c5b7
--- /dev/null
@@ -0,0 +1,79 @@
+# These brackets are redundant, therefore remove.
+try:
+    a.something
+except (AttributeError) as err:
+    raise err
+
+# This is tuple of exceptions.
+# Although this could be replaced with just the exception,
+# we do not remove brackets to preserve AST.
+try:
+    a.something
+except (AttributeError,) as err:
+    raise err
+
+# This is a tuple of exceptions. Do not remove brackets.
+try:
+    a.something
+except (AttributeError, ValueError) as err:
+    raise err
+
+# Test long variants.
+try:
+    a.something
+except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err:
+    raise err
+
+try:
+    a.something
+except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,) as err:
+    raise err
+
+try:
+    a.something
+except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err:
+    raise err
+
+# output
+# These brackets are redundant, therefore remove.
+try:
+    a.something
+except AttributeError as err:
+    raise err
+
+# This is tuple of exceptions.
+# Although this could be replaced with just the exception,
+# we do not remove brackets to preserve AST.
+try:
+    a.something
+except (AttributeError,) as err:
+    raise err
+
+# This is a tuple of exceptions. Do not remove brackets.
+try:
+    a.something
+except (AttributeError, ValueError) as err:
+    raise err
+
+# Test long variants.
+try:
+    a.something
+except (
+    some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error
+) as err:
+    raise err
+
+try:
+    a.something
+except (
+    some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,
+) as err:
+    raise err
+
+try:
+    a.something
+except (
+    some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,
+    some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,
+) as err:
+    raise err
diff --git a/.vim/bundle/black/tests/data/cases/remove_for_brackets.py b/.vim/bundle/black/tests/data/cases/remove_for_brackets.py
new file mode 100644 (file)
index 0000000..cd53404
--- /dev/null
@@ -0,0 +1,48 @@
+# Only remove tuple brackets after `for`
+for (k, v) in d.items():
+    print(k, v)
+
+# Don't touch tuple brackets after `in`
+for module in (core, _unicodefun):
+    if hasattr(module, "_verify_python3_env"):
+        module._verify_python3_env = lambda: None
+
+# Brackets remain for long for loop lines
+for (why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do) in d.items():
+    print(k, v)
+
+for (k, v) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items():
+    print(k, v)
+
+# Test deeply nested brackets
+for (((((k, v))))) in d.items():
+    print(k, v)
+
+# output
+# Only remove tuple brackets after `for`
+for k, v in d.items():
+    print(k, v)
+
+# Don't touch tuple brackets after `in`
+for module in (core, _unicodefun):
+    if hasattr(module, "_verify_python3_env"):
+        module._verify_python3_env = lambda: None
+
+# Brackets remain for long for loop lines
+for (
+    why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long,
+    i_dont_know_but_we_should_still_check_the_behaviour_if_they_do,
+) in d.items():
+    print(k, v)
+
+for (
+    k,
+    v,
+) in (
+    dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items()
+):
+    print(k, v)
+
+# Test deeply nested brackets
+for k, v in d.items():
+    print(k, v)
diff --git a/.vim/bundle/black/tests/data/cases/remove_newline_after_code_block_open.py b/.vim/bundle/black/tests/data/cases/remove_newline_after_code_block_open.py
new file mode 100644 (file)
index 0000000..ef2e5c2
--- /dev/null
@@ -0,0 +1,189 @@
+import random
+
+
+def foo1():
+
+    print("The newline above me should be deleted!")
+
+
+def foo2():
+
+
+
+    print("All the newlines above me should be deleted!")
+
+
+def foo3():
+
+    print("No newline above me!")
+
+    print("There is a newline above me, and that's OK!")
+
+
+def foo4():
+
+    # There is a comment here
+
+    print("The newline above me should not be deleted!")
+
+
+class Foo:
+    def bar(self):
+
+        print("The newline above me should be deleted!")
+
+
+for i in range(5):
+
+    print(f"{i}) The line above me should be removed!")
+
+
+for i in range(5):
+
+
+
+    print(f"{i}) The lines above me should be removed!")
+
+
+for i in range(5):
+
+    for j in range(7):
+
+        print(f"{i}) The lines above me should be removed!")
+
+
+if random.randint(0, 3) == 0:
+
+    print("The new line above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+
+
+
+
+    print("The new lines above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+    if random.uniform(0, 1) > 0.5:
+        print("Two lines above me are about to be removed!")
+
+
+while True:
+
+    print("The newline above me should be deleted!")
+
+
+while True:
+
+
+
+    print("The newlines above me should be deleted!")
+
+
+while True:
+
+    while False:
+
+        print("The newlines above me should be deleted!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+
+    file.write("The new line above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+
+
+
+    file.write("The new lines above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="r") as read_file:
+
+    with open("/path/to/output_file.txt", mode="w") as write_file:
+
+        write_file.writelines(read_file.readlines())
+
+# output
+
+import random
+
+
+def foo1():
+    print("The newline above me should be deleted!")
+
+
+def foo2():
+    print("All the newlines above me should be deleted!")
+
+
+def foo3():
+    print("No newline above me!")
+
+    print("There is a newline above me, and that's OK!")
+
+
+def foo4():
+    # There is a comment here
+
+    print("The newline above me should not be deleted!")
+
+
+class Foo:
+    def bar(self):
+        print("The newline above me should be deleted!")
+
+
+for i in range(5):
+    print(f"{i}) The line above me should be removed!")
+
+
+for i in range(5):
+    print(f"{i}) The lines above me should be removed!")
+
+
+for i in range(5):
+    for j in range(7):
+        print(f"{i}) The lines above me should be removed!")
+
+
+if random.randint(0, 3) == 0:
+    print("The new line above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+    print("The new lines above me is about to be removed!")
+
+
+if random.randint(0, 3) == 0:
+    if random.uniform(0, 1) > 0.5:
+        print("Two lines above me are about to be removed!")
+
+
+while True:
+    print("The newline above me should be deleted!")
+
+
+while True:
+    print("The newlines above me should be deleted!")
+
+
+while True:
+    while False:
+        print("The newlines above me should be deleted!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+    file.write("The new line above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="w") as file:
+    file.write("The new lines above me is about to be removed!")
+
+
+with open("/path/to/file.txt", mode="r") as read_file:
+    with open("/path/to/output_file.txt", mode="w") as write_file:
+        write_file.writelines(read_file.readlines())
diff --git a/.vim/bundle/black/tests/data/cases/remove_newline_after_match.py b/.vim/bundle/black/tests/data/cases/remove_newline_after_match.py
new file mode 100644 (file)
index 0000000..fe6592b
--- /dev/null
@@ -0,0 +1,35 @@
+# flags: --minimum-version=3.10
+def http_status(status):
+
+    match status:
+
+        case 400:
+
+            return "Bad request"
+
+        case 401:
+
+            return "Unauthorized"
+
+        case 403:
+
+            return "Forbidden"
+
+        case 404:
+
+            return "Not found"
+
+# output
+def http_status(status):
+    match status:
+        case 400:
+            return "Bad request"
+
+        case 401:
+            return "Unauthorized"
+
+        case 403:
+            return "Forbidden"
+
+        case 404:
+            return "Not found"
\ No newline at end of file
diff --git a/.vim/bundle/black/tests/data/cases/remove_with_brackets.py b/.vim/bundle/black/tests/data/cases/remove_with_brackets.py
new file mode 100644 (file)
index 0000000..3ee6490
--- /dev/null
@@ -0,0 +1,120 @@
+# flags: --minimum-version=3.9
+with (open("bla.txt")):
+    pass
+
+with (open("bla.txt")), (open("bla.txt")):
+    pass
+
+with (open("bla.txt") as f):
+    pass
+
+# Remove brackets within alias expression
+with (open("bla.txt")) as f:
+    pass
+
+# Remove brackets around one-line context managers
+with (open("bla.txt") as f, (open("x"))):
+    pass
+
+with ((open("bla.txt")) as f, open("x")):
+    pass
+
+with (CtxManager1() as example1, CtxManager2() as example2):
+    ...
+
+# Brackets remain when using magic comma
+with (CtxManager1() as example1, CtxManager2() as example2,):
+    ...
+
+# Brackets remain for multi-line context managers
+with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2):
+    ...
+
+# Don't touch assignment expressions
+with (y := open("./test.py")) as f:
+    pass
+
+# Deeply nested examples
+# N.B. Multiple brackets are only possible
+# around the context manager itself.
+# Only one brackets is allowed around the
+# alias expression or comma-delimited context managers.
+with (((open("bla.txt")))):
+    pass
+
+with (((open("bla.txt")))), (((open("bla.txt")))):
+    pass
+
+with (((open("bla.txt")))) as f:
+    pass
+
+with ((((open("bla.txt")))) as f):
+    pass
+
+with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2):
+    ...
+
+# output
+with open("bla.txt"):
+    pass
+
+with open("bla.txt"), open("bla.txt"):
+    pass
+
+with open("bla.txt") as f:
+    pass
+
+# Remove brackets within alias expression
+with open("bla.txt") as f:
+    pass
+
+# Remove brackets around one-line context managers
+with open("bla.txt") as f, open("x"):
+    pass
+
+with open("bla.txt") as f, open("x"):
+    pass
+
+with CtxManager1() as example1, CtxManager2() as example2:
+    ...
+
+# Brackets remain when using magic comma
+with (
+    CtxManager1() as example1,
+    CtxManager2() as example2,
+):
+    ...
+
+# Brackets remain for multi-line context managers
+with (
+    CtxManager1() as example1,
+    CtxManager2() as example2,
+    CtxManager2() as example2,
+    CtxManager2() as example2,
+    CtxManager2() as example2,
+):
+    ...
+
+# Don't touch assignment expressions
+with (y := open("./test.py")) as f:
+    pass
+
+# Deeply nested examples
+# N.B. Multiple brackets are only possible
+# around the context manager itself.
+# Only one brackets is allowed around the
+# alias expression or comma-delimited context managers.
+with open("bla.txt"):
+    pass
+
+with open("bla.txt"), open("bla.txt"):
+    pass
+
+with open("bla.txt") as f:
+    pass
+
+with open("bla.txt") as f:
+    pass
+
+with CtxManager1() as example1, CtxManager2() as example2:
+    ...
diff --git a/.vim/bundle/black/tests/data/cases/return_annotation_brackets.py b/.vim/bundle/black/tests/data/cases/return_annotation_brackets.py
new file mode 100644 (file)
index 0000000..8509ecd
--- /dev/null
@@ -0,0 +1,223 @@
+# Control
+def double(a: int) -> int:
+    return 2*a
+
+# Remove the brackets
+def double(a: int) -> (int):
+    return 2*a
+
+# Some newline variations
+def double(a: int) -> (
+    int):
+    return 2*a
+
+def double(a: int) -> (int
+):
+    return 2*a
+
+def double(a: int) -> (
+    int
+):
+    return 2*a
+
+# Don't lose the comments
+def double(a: int) -> ( # Hello
+    int
+):
+    return 2*a
+
+def double(a: int) -> (
+    int # Hello
+):
+    return 2*a
+
+# Really long annotations
+def foo() -> (
+    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+):
+    return 2
+
+def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds:
+    return 2
+
+def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds:
+    return 2
+
+def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds:
+    return 2
+
+def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds:
+    return 2
+
+# Split args but no need to split return
+def foo(a: int, b: int, c: int,) -> int:
+    return 2
+
+# Deeply nested brackets
+# with *interesting* spacing
+def double(a: int) -> (((((int))))):
+    return 2*a
+
+def double(a: int) -> (
+    (  (
+        ((int)
+         )
+           )
+            )
+        ):
+    return 2*a
+
+def foo() -> (
+    (  (
+    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+)
+)):
+    return 2
+
+# Return type with commas
+def foo() -> (
+    tuple[int, int, int]
+):
+    return 2
+
+def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong]:
+    return 2
+
+# Magic trailing comma example
+def foo() -> tuple[int, int, int,]:
+    return 2
+
+# Magic trailing comma example, with params
+# this is broken - the trailing comma is transferred to the param list. Fixed in preview
+def foo(a,b) -> tuple[int, int, int,]:
+    return 2
+
+# output
+# Control
+def double(a: int) -> int:
+    return 2 * a
+
+
+# Remove the brackets
+def double(a: int) -> int:
+    return 2 * a
+
+
+# Some newline variations
+def double(a: int) -> int:
+    return 2 * a
+
+
+def double(a: int) -> int:
+    return 2 * a
+
+
+def double(a: int) -> int:
+    return 2 * a
+
+
+# Don't lose the comments
+def double(a: int) -> int:  # Hello
+    return 2 * a
+
+
+def double(a: int) -> int:  # Hello
+    return 2 * a
+
+
+# Really long annotations
+def foo() -> (
+    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+):
+    return 2
+
+
+def foo() -> (
+    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+):
+    return 2
+
+
+def foo() -> (
+    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+    | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+):
+    return 2
+
+
+def foo(
+    a: int,
+    b: int,
+    c: int,
+) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds:
+    return 2
+
+
+def foo(
+    a: int,
+    b: int,
+    c: int,
+) -> (
+    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+    | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+):
+    return 2
+
+
+# Split args but no need to split return
+def foo(
+    a: int,
+    b: int,
+    c: int,
+) -> int:
+    return 2
+
+
+# Deeply nested brackets
+# with *interesting* spacing
+def double(a: int) -> int:
+    return 2 * a
+
+
+def double(a: int) -> int:
+    return 2 * a
+
+
+def foo() -> (
+    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
+):
+    return 2
+
+
+# Return type with commas
+def foo() -> tuple[int, int, int]:
+    return 2
+
+
+def foo() -> (
+    tuple[
+        loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+        loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+        loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+    ]
+):
+    return 2
+
+
+# Magic trailing comma example
+def foo() -> (
+    tuple[
+        int,
+        int,
+        int,
+    ]
+):
+    return 2
+
+
+# Magic trailing comma example, with params
+# this is broken - the trailing comma is transferred to the param list. Fixed in preview
+def foo(
+    a, b
+) -> tuple[int, int, int,]:
+    return 2
diff --git a/.vim/bundle/black/tests/data/cases/skip_magic_trailing_comma.py b/.vim/bundle/black/tests/data/cases/skip_magic_trailing_comma.py
new file mode 100644 (file)
index 0000000..4dda5df
--- /dev/null
@@ -0,0 +1,75 @@
+# flags: --skip-magic-trailing-comma
+# We should not remove the trailing comma in a single-element subscript.
+a: tuple[int,]
+b = tuple[int,]
+
+# But commas in multiple element subscripts should be removed.
+c: tuple[int, int,]
+d = tuple[int, int,]
+
+# Remove commas for non-subscripts.
+small_list = [1,]
+list_of_types = [tuple[int,],]
+small_set = {1,}
+set_of_types = {tuple[int,],}
+
+# Except single element tuples
+small_tuple = (1,)
+
+# Trailing commas in multiple chained non-nested parens.
+zero(
+    one,
+).two(
+    three,
+).four(
+    five,
+)
+
+func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5)
+
+(
+    a,
+    b,
+    c,
+    d,
+) = func1(
+    arg1
+) and func2(arg2)
+
+func(
+    argument1,
+    (
+        one,
+        two,
+    ),
+    argument4,
+    argument5,
+    argument6,
+)
+
+# output
+# We should not remove the trailing comma in a single-element subscript.
+a: tuple[int,]
+b = tuple[int,]
+
+# But commas in multiple element subscripts should be removed.
+c: tuple[int, int]
+d = tuple[int, int]
+
+# Remove commas for non-subscripts.
+small_list = [1]
+list_of_types = [tuple[int,]]
+small_set = {1}
+set_of_types = {tuple[int,]}
+
+# Except single element tuples
+small_tuple = (1,)
+
+# Trailing commas in multiple chained non-nested parens.
+zero(one).two(three).four(five)
+
+func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5)
+
+(a, b, c, d) = func1(arg1) and func2(arg2)
+
+func(argument1, (one, two), argument4, argument5, argument6)
similarity index 94%
rename from .vim/bundle/black/tests/data/slices.py
rename to .vim/bundle/black/tests/data/cases/slices.py
index 7a42678f646fcfc523d82f3e1e3b5173b7daeceb..165117cdcb495be4470d8cbe41bb4c584432380c 100644 (file)
@@ -9,7 +9,7 @@ slice[::-1]
 slice[:c, c - 1]
 slice[c, c + 1, d::]
 slice[ham[c::d] :: 1]
-slice[ham[cheese ** 2 : -1] : 1 : 1, ham[1:2]]
+slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]]
 slice[:-1:]
 slice[lambda: None : lambda: None]
 slice[lambda x, y, *args, really=2, **kwargs: None :, None::]
diff --git a/.vim/bundle/black/tests/data/cases/starred_for_target.py b/.vim/bundle/black/tests/data/cases/starred_for_target.py
new file mode 100644 (file)
index 0000000..13e5178
--- /dev/null
@@ -0,0 +1,28 @@
+# flags: --minimum-version=3.10
+for x in *a, *b:
+    print(x)
+
+for x in a, b, *c:
+    print(x)
+
+for x in *a, b, c:
+    print(x)
+
+for x in *a, b, *c:
+    print(x)
+
+async for x in *a, *b:
+    print(x)
+
+async for x in *a, b, *c:
+    print(x)
+
+async for x in a, b, *c:
+    print(x)
+
+async for x in (
+    *loooooooooooooooooooooong,
+    very,
+    *loooooooooooooooooooooooooooooooooooooooooooooooong,
+):
+    print(x)
similarity index 51%
rename from .vim/bundle/black/tests/data/string_prefixes.py
rename to .vim/bundle/black/tests/data/cases/string_prefixes.py
index 9ddc2b540fcefe3c729e198cdc9e6ba5da5f4861..f86da696e1566482850877c8ca3d1ed745ed0647 100644 (file)
@@ -1,10 +1,13 @@
-#!/usr/bin/env python3.6
+#!/usr/bin/env python3
 
-name = R"Łukasz"
-F"hello {name}"
-B"hello"
-r"hello"
-fR"hello"
+name = "Łukasz"
+(f"hello {name}", F"hello {name}")
+(b"", B"")
+(u"", U"")
+(r"", R"")
+
+(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"")
+(rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"")
 
 
 def docstring_singleline():
@@ -20,13 +23,16 @@ def docstring_multiline():
 # output
 
 
-#!/usr/bin/env python3.6
+#!/usr/bin/env python3
+
+name = "Łukasz"
+(f"hello {name}", f"hello {name}")
+(b"", b"")
+("", "")
+(r"", R"")
 
-name = R"Łukasz"
-f"hello {name}"
-b"hello"
-r"hello"
-fR"hello"
+(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"")
+(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"")
 
 
 def docstring_singleline():
diff --git a/.vim/bundle/black/tests/data/cases/stub.py b/.vim/bundle/black/tests/data/cases/stub.py
new file mode 100644 (file)
index 0000000..f3828d5
--- /dev/null
@@ -0,0 +1,152 @@
+# flags: --pyi
+X: int
+
+def f(): ...
+
+
+class D: 
+    ...
+
+
+class C:
+    ...
+
+class B:
+    this_lack_of_newline_should_be_kept: int
+    def b(self) -> None: ...
+
+    but_this_newline_should_also_be_kept: int
+
+class A:
+    attr: int
+    attr2: str
+
+    def f(self) -> int:
+        ...
+
+    def g(self) -> str: ...
+
+
+
+def g():
+    ...
+
+def h(): ...
+
+if sys.version_info >= (3, 8):
+    class E:
+        def f(self): ...
+    class F:
+
+        def f(self): ...
+    class G: ...
+    class H: ...
+else:
+    class I: ...
+    class J: ...
+    def f(): ...
+
+    class K:
+        def f(self): ...
+    def f(): ...
+
+class Nested:
+    class dirty: ...
+    class little: ...
+    class secret:
+        def who_has_to_know(self): ...
+    def verse(self): ...
+
+class Conditional:
+    def f(self): ...
+    if sys.version_info >= (3, 8):
+        def g(self): ...
+    else:
+        def g(self): ...
+    def h(self): ...
+    def i(self): ...
+    if sys.version_info >= (3, 8):
+        def j(self): ...
+    def k(self): ...
+    if sys.version_info >= (3, 8):
+        class A: ...
+        class B: ...
+        class C:
+            def l(self): ...
+            def m(self): ...
+
+
+# output
+X: int
+
+def f(): ...
+
+class D: ...
+class C: ...
+
+class B:
+    this_lack_of_newline_should_be_kept: int
+    def b(self) -> None: ...
+
+    but_this_newline_should_also_be_kept: int
+
+class A:
+    attr: int
+    attr2: str
+
+    def f(self) -> int: ...
+    def g(self) -> str: ...
+
+def g(): ...
+def h(): ...
+
+if sys.version_info >= (3, 8):
+    class E:
+        def f(self): ...
+
+    class F:
+        def f(self): ...
+
+    class G: ...
+    class H: ...
+
+else:
+    class I: ...
+    class J: ...
+
+    def f(): ...
+
+    class K:
+        def f(self): ...
+
+    def f(): ...
+
+class Nested:
+    class dirty: ...
+    class little: ...
+
+    class secret:
+        def who_has_to_know(self): ...
+
+    def verse(self): ...
+
+class Conditional:
+    def f(self): ...
+    if sys.version_info >= (3, 8):
+        def g(self): ...
+    else:
+        def g(self): ...
+
+    def h(self): ...
+    def i(self): ...
+    if sys.version_info >= (3, 8):
+        def j(self): ...
+
+    def k(self): ...
+    if sys.version_info >= (3, 8):
+        class A: ...
+        class B: ...
+
+        class C:
+            def l(self): ...
+            def m(self): ...
diff --git a/.vim/bundle/black/tests/data/cases/torture.py b/.vim/bundle/black/tests/data/cases/torture.py
new file mode 100644 (file)
index 0000000..2a19475
--- /dev/null
@@ -0,0 +1,91 @@
+importA;() << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 #
+
+assert sort_by_dependency(
+    {
+        "1": {"2", "3"}, "2": {"2a", "2b"}, "3": {"3a", "3b"},
+        "2a": set(), "2b": set(), "3a": set(), "3b": set()
+    }
+) == ["2a", "2b", "2", "3a", "3b", "3", "1"]
+
+importA
+0;0^0#
+
+class A:
+    def foo(self):
+        for _ in range(10):
+            aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(  # pylint: disable=no-member
+                xxxxxxxxxxxx
+            )
+
+def test(self, othr):
+    return (1 == 2 and
+            (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) ==
+            (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule))
+
+
+assert (
+    a_function(very_long_arguments_that_surpass_the_limit, which_is_eighty_eight_in_this_case_plus_a_bit_more)
+    == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"}
+)
+
+# output
+
+importA
+(
+    ()
+    << 0
+    ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525
+)  #
+
+assert sort_by_dependency(
+    {
+        "1": {"2", "3"},
+        "2": {"2a", "2b"},
+        "3": {"3a", "3b"},
+        "2a": set(),
+        "2b": set(),
+        "3a": set(),
+        "3b": set(),
+    }
+) == ["2a", "2b", "2", "3a", "3b", "3", "1"]
+
+importA
+0
+0 ^ 0  #
+
+
+class A:
+    def foo(self):
+        for _ in range(10):
+            aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(
+                xxxxxxxxxxxx
+            )  # pylint: disable=no-member
+
+
+def test(self, othr):
+    return 1 == 2 and (
+        name,
+        description,
+        self.default,
+        self.selected,
+        self.auto_generated,
+        self.parameters,
+        self.meta_data,
+        self.schedule,
+    ) == (
+        name,
+        description,
+        othr.default,
+        othr.selected,
+        othr.auto_generated,
+        othr.parameters,
+        othr.meta_data,
+        othr.schedule,
+    )
+
+
+assert a_function(
+    very_long_arguments_that_surpass_the_limit,
+    which_is_eighty_eight_in_this_case_plus_a_bit_more,
+) == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"}
+
diff --git a/.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens1.py b/.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens1.py
new file mode 100644 (file)
index 0000000..85aa8ba
--- /dev/null
@@ -0,0 +1,63 @@
+if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT,
+                        _winapi.ERROR_PIPE_BUSY) or _check_timeout(t):
+    pass
+
+if x:
+    if y:
+        new_id = max(Vegetable.objects.order_by('-id')[0].id,
+                     Mineral.objects.order_by('-id')[0].id) + 1
+
+class X:
+    def get_help_text(self):
+        return ngettext(
+            "Your password must contain at least %(min_length)d character.",
+            "Your password must contain at least %(min_length)d characters.",
+            self.min_length,
+        ) % {'min_length': self.min_length}
+
+class A:
+    def b(self):
+        if self.connection.mysql_is_mariadb and (
+            10,
+            4,
+            3,
+        ) < self.connection.mysql_version < (10, 5, 2):
+            pass
+
+
+# output
+
+if e1234123412341234.winerror not in (
+    _winapi.ERROR_SEM_TIMEOUT,
+    _winapi.ERROR_PIPE_BUSY,
+) or _check_timeout(t):
+    pass
+
+if x:
+    if y:
+        new_id = (
+            max(
+                Vegetable.objects.order_by("-id")[0].id,
+                Mineral.objects.order_by("-id")[0].id,
+            )
+            + 1
+        )
+
+
+class X:
+    def get_help_text(self):
+        return ngettext(
+            "Your password must contain at least %(min_length)d character.",
+            "Your password must contain at least %(min_length)d characters.",
+            self.min_length,
+        ) % {"min_length": self.min_length}
+
+
+class A:
+    def b(self):
+        if self.connection.mysql_is_mariadb and (
+            10,
+            4,
+            3,
+        ) < self.connection.mysql_version < (10, 5, 2):
+            pass
diff --git a/.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens2.py b/.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens2.py
new file mode 100644 (file)
index 0000000..9541670
--- /dev/null
@@ -0,0 +1,12 @@
+if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or
+    (8, 5, 8) <= get_tk_patchlevel() < (8, 6)):
+    pass
+
+# output
+
+if e123456.get_tk_patchlevel() >= (8, 6, 0, "final") or (
+    8,
+    5,
+    8,
+) <= get_tk_patchlevel() < (8, 6):
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens3.py b/.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens3.py
new file mode 100644 (file)
index 0000000..c0ed699
--- /dev/null
@@ -0,0 +1,21 @@
+if True:
+    if True:
+        if True:
+            return _(
+                "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas "
+                + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.",
+                "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe",
+            ) % {"reported_username": reported_username, "report_reason": report_reason}
+
+
+# output
+
+
+if True:
+    if True:
+        if True:
+            return _(
+                "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas "
+                + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.",
+                "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe",
+            ) % {"reported_username": reported_username, "report_reason": report_reason}
diff --git a/.vim/bundle/black/tests/data/cases/trailing_commas_in_leading_parts.py b/.vim/bundle/black/tests/data/cases/trailing_commas_in_leading_parts.py
new file mode 100644 (file)
index 0000000..99d82a6
--- /dev/null
@@ -0,0 +1,88 @@
+zero(one,).two(three,).four(five,)
+
+func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5)
+
+# Inner one-element tuple shouldn't explode
+func1(arg1).func2(arg1, (one_tuple,)).func3(arg3)
+
+(a, b, c, d,) = func1(arg1) and func2(arg2)
+
+
+# Example from https://github.com/psf/black/issues/3229
+def refresh_token(self, device_family, refresh_token, api_key):
+    return self.orchestration.refresh_token(
+        data={
+            "refreshToken": refresh_token,
+        },
+        api_key=api_key,
+    )["extensions"]["sdk"]["token"]
+
+
+# Edge case where a bug in a working-in-progress version of
+# https://github.com/psf/black/pull/3370 causes an infinite recursion.
+assert (
+    long_module.long_class.long_func().another_func()
+    == long_module.long_class.long_func()["some_key"].another_func(arg1)
+)
+
+# Regression test for https://github.com/psf/black/issues/3414.
+assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(
+    xxxxxxxxx
+).xxxxxxxxxxxxxxxxxx(), (
+    "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+)
+
+
+# output
+
+
+zero(
+    one,
+).two(
+    three,
+).four(
+    five,
+)
+
+func1(arg1).func2(
+    arg2,
+).func3(arg3).func4(
+    arg4,
+).func5(arg5)
+
+# Inner one-element tuple shouldn't explode
+func1(arg1).func2(arg1, (one_tuple,)).func3(arg3)
+
+(
+    a,
+    b,
+    c,
+    d,
+) = func1(
+    arg1
+) and func2(arg2)
+
+
+# Example from https://github.com/psf/black/issues/3229
+def refresh_token(self, device_family, refresh_token, api_key):
+    return self.orchestration.refresh_token(
+        data={
+            "refreshToken": refresh_token,
+        },
+        api_key=api_key,
+    )["extensions"]["sdk"]["token"]
+
+
+# Edge case where a bug in a working-in-progress version of
+# https://github.com/psf/black/pull/3370 causes an infinite recursion.
+assert (
+    long_module.long_class.long_func().another_func()
+    == long_module.long_class.long_func()["some_key"].another_func(arg1)
+)
+
+# Regression test for https://github.com/psf/black/issues/3414.
+assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx(
+    xxxxxxxxx
+).xxxxxxxxxxxxxxxxxx(), (
+    "xxx {xxxxxxxxx} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+)
similarity index 76%
rename from .vim/bundle/black/tests/data/tricky_unicode_symbols.py
rename to .vim/bundle/black/tests/data/cases/tricky_unicode_symbols.py
index 366a92fa9d4a8e860ee6a5780fc672a2e3fb98aa..ad8b610859031ec17b2d2a3d40b53b53f27eb4a3 100644 (file)
@@ -4,3 +4,6 @@
 x󠄀 = 4
 មុ = 1
 Q̇_per_meter = 4
+
+A᧚ = 3
+A፩ = 8
diff --git a/.vim/bundle/black/tests/data/cases/type_aliases.py b/.vim/bundle/black/tests/data/cases/type_aliases.py
new file mode 100644 (file)
index 0000000..7c2009e
--- /dev/null
@@ -0,0 +1,30 @@
+# flags: --minimum-version=3.12
+
+type A=int
+type Gen[T]=list[T]
+type Alias[T]=lambda: T
+type And[T]=T and T
+type IfElse[T]=T if T else T
+type One = int; type Another = str
+class X: type InClass = int
+
+type = aliased
+print(type(42))
+
+# output
+
+type A = int
+type Gen[T] = list[T]
+type Alias[T] = lambda: T
+type And[T] = T and T
+type IfElse[T] = T if T else T
+type One = int
+type Another = str
+
+
+class X:
+    type InClass = int
+
+
+type = aliased
+print(type(42))
diff --git a/.vim/bundle/black/tests/data/cases/type_comment_syntax_error.py b/.vim/bundle/black/tests/data/cases/type_comment_syntax_error.py
new file mode 100644 (file)
index 0000000..2e5ca2e
--- /dev/null
@@ -0,0 +1,11 @@
+def foo(
+    # type: Foo
+    x): pass
+
+# output
+
+def foo(
+    # type: Foo
+    x,
+):
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/type_params.py b/.vim/bundle/black/tests/data/cases/type_params.py
new file mode 100644 (file)
index 0000000..720a775
--- /dev/null
@@ -0,0 +1,58 @@
+# flags: --minimum-version=3.12
+def func  [T ](): pass
+async def func [ T ] (): pass
+class C[ T ] : pass
+
+def all_in[T   :   int,U : (bytes, str),*   Ts,**P](): pass
+
+def really_long[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine](): pass
+
+def even_longer[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine: WhatIfItHadABound](): pass
+
+def it_gets_worse[WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine, ItCouldBeGenericOverMultipleTypeVars](): pass
+
+def magic[Trailing, Comma,](): pass
+
+# output
+
+
+def func[T]():
+    pass
+
+
+async def func[T]():
+    pass
+
+
+class C[T]:
+    pass
+
+
+def all_in[T: int, U: (bytes, str), *Ts, **P]():
+    pass
+
+
+def really_long[
+    WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine
+]():
+    pass
+
+
+def even_longer[
+    WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine: WhatIfItHadABound
+]():
+    pass
+
+
+def it_gets_worse[
+    WhatIsTheLongestTypeVarNameYouCanThinkOfEnoughToMakeBlackSplitThisLine,
+    ItCouldBeGenericOverMultipleTypeVars,
+]():
+    pass
+
+
+def magic[
+    Trailing,
+    Comma,
+]():
+    pass
diff --git a/.vim/bundle/black/tests/data/cases/whitespace.py b/.vim/bundle/black/tests/data/cases/whitespace.py
new file mode 100644 (file)
index 0000000..a319c01
--- /dev/null
@@ -0,0 +1,6 @@
+
+               
+
+           
+
+# output
diff --git a/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/.gitignore b/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/.gitignore
new file mode 100644 (file)
index 0000000..2987e7b
--- /dev/null
@@ -0,0 +1 @@
+a.py
diff --git a/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir1/b.py b/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir1/b.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/a.py b/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/a.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/b.py b/.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/b.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/a.py b/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/a.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore b/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore
new file mode 100644 (file)
index 0000000..150f68c
--- /dev/null
@@ -0,0 +1 @@
+*/*
diff --git a/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py b/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py b/.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py
new file mode 100644 (file)
index 0000000..e69de29
similarity index 91%
rename from .vim/bundle/black/tests/data/expression_skip_magic_trailing_comma.diff
rename to .vim/bundle/black/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff
index 4a8a95c72371b70213f8eb65589aada239ca1414..d17467b15c71bfa9ea267ef6ae0f8d7b8ca38e03 100644 (file)
  True
  False
  1
-@@ -29,63 +29,84 @@
+@@ -21,99 +21,118 @@
+ Name1 or (Name2 and Name3) or Name4
+ Name1 or Name2 and Name3 or Name4
+ v1 << 2
+ 1 >> v2
+ 1 % finished
+-1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8
+-((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8)
++1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8
++((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8)
+ not great
  ~great
  +value
  -1
@@ -19,7 +29,7 @@
  (~int) and (not ((v1 ^ (123 + v2)) | True))
 -+really ** -confusing ** ~operator ** -precedence
 -flags & ~ select.EPOLLIN and waiters.write_task is not None
-++(really ** -(confusing ** ~(operator ** -precedence)))
+++(really ** -(confusing ** ~(operator**-precedence)))
 +flags & ~select.EPOLLIN and waiters.write_task is not None
  lambda arg: None
  lambda a=True: a
 +    *more,
 +]
  {i for i in (1, 2, 3)}
- {(i ** 2) for i in (1, 2, 3)}
+-{(i ** 2) for i in (1, 2, 3)}
 -{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}
-+{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
- {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
+-{((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
++{(i**2) for i in (1, 2, 3)}
++{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
++{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
  [i for i in (1, 2, 3)]
- [(i ** 2) for i in (1, 2, 3)]
+-[(i ** 2) for i in (1, 2, 3)]
 -[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))]
-+[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
- [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
+-[((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
++[(i**2) for i in (1, 2, 3)]
++[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
++[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
  {i: 0 for i in (1, 2, 3)}
 -{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}
 +{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))}
  call(**self.screen_kwargs)
  call(b, **self.screen_kwargs)
  lukasz.langa.pl
-@@ -94,26 +115,24 @@
- 1.0 .real
+ call.me(maybe)
+-1 .real
+-1.0 .real
++(1).real
++(1.0).real
  ....__class__
  list[str]
  dict[str, int]
 -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]]
- 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]]]],
  ]
  SomeName
  (Good, Bad, Ugly)
  (i for i in (1, 2, 3))
- ((i ** 2) for i in (1, 2, 3))
+-((i ** 2) for i in (1, 2, 3))
 -((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c')))
-+((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
- (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
+-(((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
++((i**2) for i in (1, 2, 3))
++((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
++(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
  (*starred,)
 -{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs}
 +{
 +    return True
 +if (
 +    ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e
-+    | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n
++    | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n
 +):
 +    return True
 +if (
 +    ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e
 +    | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h
-+    ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n
++    ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n
 +):
 +    return True
 +if (
 +    | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h
 +    ^ aaaaaaaaaaaaaaaa.i
 +    << aaaaaaaaaaaaaaaa.k
-+    >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
++    >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
 +):
 +    return True
 +(
similarity index 88%
rename from .vim/bundle/black/tests/data/force_py36.py
rename to .vim/bundle/black/tests/data/miscellaneous/force_py36.py
index cad935e525a46057349becfcc1845ba37b2b28e3..4c9b70336e748ecc7c0384338ee7606b6496c95f 100644 (file)
@@ -1,6 +1,6 @@
 # The input source must not contain any Py36-specific syntax (e.g. argument type
 # annotations, trailing comma after *rest) or this test becomes invalid.
-def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): ...
+def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): pass
 # output
 # The input source must not contain any Py36-specific syntax (e.g. argument type
 # annotations, trailing comma after *rest) or this test becomes invalid.
@@ -13,4 +13,4 @@ def long_function_name(
     argument_six,
     *rest,
 ):
-    ...
+    pass
similarity index 98%
rename from .vim/bundle/black/tests/data/force_pyi.py
rename to .vim/bundle/black/tests/data/miscellaneous/force_pyi.py
index 07ed93c6879363f22cde9de9e3c3c393aa2d41a1..40caf30a9831ded54ce91b8d0d4932d07a9c517a 100644 (file)
@@ -1,3 +1,4 @@
+# flags: --pyi
 from typing import Union
 
 @bird
diff --git a/.vim/bundle/black/tests/data/miscellaneous/invalid_header.py b/.vim/bundle/black/tests/data/miscellaneous/invalid_header.py
new file mode 100644 (file)
index 0000000..fb49e2f
--- /dev/null
@@ -0,0 +1,2 @@
+This is not valid Python syntax
+y = "This is valid syntax"
diff --git a/.vim/bundle/black/tests/data/miscellaneous/pattern_matching_invalid.py b/.vim/bundle/black/tests/data/miscellaneous/pattern_matching_invalid.py
new file mode 100644 (file)
index 0000000..22b5b94
--- /dev/null
@@ -0,0 +1,18 @@
+# First match, no errors
+match something:
+    case bla():
+        pass
+
+# Problem on line 10
+match invalid_case:
+    case valid_case:
+        pass
+    case a := b:
+        pass
+    case valid_case:
+        pass
+
+# No problems either
+match something:
+    case bla():
+        pass
diff --git a/.vim/bundle/black/tests/data/miscellaneous/python2_detection.py b/.vim/bundle/black/tests/data/miscellaneous/python2_detection.py
new file mode 100644 (file)
index 0000000..8de2bb5
--- /dev/null
@@ -0,0 +1,90 @@
+# This uses a similar construction to the decorators.py test data file FYI.
+
+print "hello, world!"
+
+###
+
+exec "print('hello, world!')"
+
+###
+
+def set_position((x, y), value):
+    pass
+
+###
+
+try:
+    pass
+except Exception, err:
+    pass
+
+###
+
+raise RuntimeError, "I feel like crashing today :p"
+
+###
+
+`wow_these_really_did_exist`
+
+###
+
+10L
+
+###
+
+10l
+
+###
+
+0123
+
+# output
+
+print("hello python three!")
+
+###
+
+exec("I'm not sure if you can use exec like this but that's not important here!")
+
+###
+
+try:
+    pass
+except make_exception(1, 2):
+    pass
+
+###
+
+try:
+    pass
+except Exception as err:
+    pass
+
+###
+
+raise RuntimeError(make_msg(1, 2))
+
+###
+
+raise RuntimeError("boom!",)
+
+###
+
+def set_position(x, y, value):
+    pass
+
+###
+
+10
+
+###
+
+0
+
+###
+
+000
+
+###
+
+0o12
\ No newline at end of file
similarity index 99%
rename from .vim/bundle/black/tests/data/string_quotes.py
rename to .vim/bundle/black/tests/data/miscellaneous/string_quotes.py
index 3384241f4adaf7ad8e1dbff5be6da70e32a40558..6ec088ac79b79a6f135ee53dcd575ac1e608e0c6 100644 (file)
@@ -1,4 +1,5 @@
 ''''''
+
 '\''
 '"'
 "'"
@@ -59,6 +60,7 @@ f"\"{a}\"{'hello' * b}\"{c}\""
 # output
 
 """"""
+
 "'"
 '"'
 "'"
diff --git a/.vim/bundle/black/tests/data/numeric_literals_py2.py b/.vim/bundle/black/tests/data/numeric_literals_py2.py
deleted file mode 100644 (file)
index 8f85c43..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env python2.7
-
-x = 123456789L
-x = 123456789l
-x = 123456789
-x = 0xb1acc
-
-# output
-
-
-#!/usr/bin/env python2.7
-
-x = 123456789L
-x = 123456789L
-x = 123456789
-x = 0xB1ACC
diff --git a/.vim/bundle/black/tests/data/pep_572_py310.py b/.vim/bundle/black/tests/data/pep_572_py310.py
deleted file mode 100644 (file)
index 2aef589..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-# Unparenthesized walruses are now allowed in indices since Python 3.10.
-x[a:=0]
-x[a:=0, b:=1]
-x[5, b:=0]
diff --git a/.vim/bundle/black/tests/data/project_metadata/both_pyproject.toml b/.vim/bundle/black/tests/data/project_metadata/both_pyproject.toml
new file mode 100644 (file)
index 0000000..cf8f148
--- /dev/null
@@ -0,0 +1,8 @@
+[project]
+name = "test"
+version = "1.0.0"
+requires-python = ">=3.7,<3.11"
+
+[tool.black]
+line-length = 79
+target-version = ["py310"]
diff --git a/.vim/bundle/black/tests/data/project_metadata/neither_pyproject.toml b/.vim/bundle/black/tests/data/project_metadata/neither_pyproject.toml
new file mode 100644 (file)
index 0000000..67623d2
--- /dev/null
@@ -0,0 +1,6 @@
+[project]
+name = "test"
+version = "1.0.0"
+
+[tool.black]
+line-length = 79
diff --git a/.vim/bundle/black/tests/data/project_metadata/only_black_pyproject.toml b/.vim/bundle/black/tests/data/project_metadata/only_black_pyproject.toml
new file mode 100644 (file)
index 0000000..94058bb
--- /dev/null
@@ -0,0 +1,7 @@
+[project]
+name = "test"
+version = "1.0.0"
+
+[tool.black]
+line-length = 79
+target-version = ["py310"]
diff --git a/.vim/bundle/black/tests/data/project_metadata/only_metadata_pyproject.toml b/.vim/bundle/black/tests/data/project_metadata/only_metadata_pyproject.toml
new file mode 100644 (file)
index 0000000..1c8cdbb
--- /dev/null
@@ -0,0 +1,7 @@
+[project]
+name = "test"
+version = "1.0.0"
+requires-python = ">=3.7,<3.11"
+
+[tool.black]
+line-length = 79
diff --git a/.vim/bundle/black/tests/data/python2.py b/.vim/bundle/black/tests/data/python2.py
deleted file mode 100644 (file)
index 4a22f46..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python2
-
-import sys
-
-print >> sys.stderr , "Warning:" ,
-print >> sys.stderr , "this is a blast from the past."
-print >> sys.stderr , "Look, a repr:", `sys`
-
-
-def function((_globals, _locals)):
-    exec ur"print 'hi from exec!'" in _globals, _locals
-
-
-function((globals(), locals()))
-
-
-# output
-
-
-#!/usr/bin/env python2
-
-import sys
-
-print >>sys.stderr, "Warning:",
-print >>sys.stderr, "this is a blast from the past."
-print >>sys.stderr, "Look, a repr:", ` sys `
-
-
-def function((_globals, _locals)):
-    exec ur"print 'hi from exec!'" in _globals, _locals
-
-
-function((globals(), locals()))
diff --git a/.vim/bundle/black/tests/data/python2_print_function.py b/.vim/bundle/black/tests/data/python2_print_function.py
deleted file mode 100755 (executable)
index 81b8d8a..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env python2
-from __future__ import print_function
-
-print('hello')
-print(u'hello')
-print(a, file=sys.stderr)
-
-# output
-
-
-#!/usr/bin/env python2
-from __future__ import print_function
-
-print("hello")
-print(u"hello")
-print(a, file=sys.stderr)
diff --git a/.vim/bundle/black/tests/data/python2_unicode_literals.py b/.vim/bundle/black/tests/data/python2_unicode_literals.py
deleted file mode 100644 (file)
index 2fe7039..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python2
-from __future__ import unicode_literals as _unicode_literals
-from __future__ import absolute_import
-from __future__ import print_function as lol, with_function
-
-u'hello'
-U"hello"
-Ur"hello"
-
-# output
-
-
-#!/usr/bin/env python2
-from __future__ import unicode_literals as _unicode_literals
-from __future__ import absolute_import
-from __future__ import print_function as lol, with_function
-
-"hello"
-"hello"
-r"hello"
diff --git a/.vim/bundle/black/tests/data/raw_docstring.py b/.vim/bundle/black/tests/data/raw_docstring.py
new file mode 100644 (file)
index 0000000..751fd32
--- /dev/null
@@ -0,0 +1,32 @@
+# flags: --preview --skip-string-normalization
+class C:
+
+    r"""Raw"""
+
+def f():
+
+    r"""Raw"""
+
+class SingleQuotes:
+
+
+    r'''Raw'''
+
+class UpperCaseR:
+    R"""Raw"""
+
+# output
+class C:
+    r"""Raw"""
+
+
+def f():
+    r"""Raw"""
+
+
+class SingleQuotes:
+    r'''Raw'''
+
+
+class UpperCaseR:
+    R"""Raw"""
diff --git a/.vim/bundle/black/tests/data/stub.pyi b/.vim/bundle/black/tests/data/stub.pyi
deleted file mode 100644 (file)
index 94ba852..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-X: int
-
-def f(): ...
-
-class C:
-    ...
-
-class B:
-    ...
-
-class A:
-    def f(self) -> int:
-        ...
-
-    def g(self) -> str: ...
-
-def g():
-    ...
-
-def h(): ...
-
-# output
-X: int
-
-def f(): ...
-
-class C: ...
-class B: ...
-
-class A:
-    def f(self) -> int: ...
-    def g(self) -> str: ...
-
-def g(): ...
-def h(): ...
diff --git a/.vim/bundle/black/tests/data/trailing_comma_optional_parens1.py b/.vim/bundle/black/tests/data/trailing_comma_optional_parens1.py
deleted file mode 100644 (file)
index 5ad29a8..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-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/.vim/bundle/black/tests/data/trailing_comma_optional_parens2.py b/.vim/bundle/black/tests/data/trailing_comma_optional_parens2.py
deleted file mode 100644 (file)
index 2817073..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-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/.vim/bundle/black/tests/data/trailing_comma_optional_parens3.py b/.vim/bundle/black/tests/data/trailing_comma_optional_parens3.py
deleted file mode 100644 (file)
index e6a673e..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-if True:
-    if True:
-        if True:
-            return _(
-                "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas "
-                + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.",
-                "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe",
-            ) % {"reported_username": reported_username, "report_reason": report_reason}
\ No newline at end of file
index e12b94cd29e47acf24a6f7b6cabefa7948526525..3f5277b6b034c0b513b9cbd146cdc796680a95c0 100644 (file)
@@ -14,27 +14,32 @@ Specifying the name of the default behavior in `--run-optional=` is harmless.
 Adapted from https://pypi.org/project/pytest-optional-tests/, (c) 2019 Reece Hart
 """
 
-from functools import lru_cache
 import itertools
 import logging
 import re
-from typing import FrozenSet, List, Set, TYPE_CHECKING
+from functools import lru_cache
+from typing import TYPE_CHECKING, FrozenSet, List, Set
 
 import pytest
-from _pytest.store import StoreKey
+
+try:
+    from pytest import StashKey
+except ImportError:
+    # pytest < 7
+    from _pytest.store import StoreKey as StashKey  # type: ignore[import, no-redef]
 
 log = logging.getLogger(__name__)
 
 
 if TYPE_CHECKING:
-    from _pytest.config.argparsing import Parser
     from _pytest.config import Config
+    from _pytest.config.argparsing import Parser
     from _pytest.mark.structures import MarkDecorator
     from _pytest.nodes import Node
 
 
-ALL_POSSIBLE_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]()
-ENABLED_OPTIONAL_MARKERS = StoreKey[FrozenSet[str]]()
+ALL_POSSIBLE_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]()
+ENABLED_OPTIONAL_MARKERS = StashKey[FrozenSet[str]]()
 
 
 def pytest_addoption(parser: "Parser") -> None:
@@ -96,7 +101,7 @@ def pytest_collection_modifyitems(config: "Config", items: "List[Node]") -> None
     enabled_optional_markers = store[ENABLED_OPTIONAL_MARKERS]
 
     for item in items:
-        all_markers_on_test = set(m.name for m in item.iter_markers())
+        all_markers_on_test = {m.name for m in item.iter_markers()}
         optional_markers_on_test = all_markers_on_test & all_possible_optional_markers
         if not optional_markers_on_test or (
             optional_markers_on_test & enabled_optional_markers
index d3ab1e6120211f65f6fdfd9cfb00b2c5f52adb2d..e5fb9228f19b2e50a89c7c43d58961df8a916b9b 100644 (file)
@@ -7,6 +7,7 @@ line-length = 79
 target-version = ["py36", "py37", "py38"]
 exclude='\.pyi?$'
 include='\.py?$'
+python-cell-magics = ["custom1", "custom2"]
 
 [v1.0.0-syntax]
 # This shouldn't break Black.
index beb56cf1fdb24bde7256366f0aa1ebf7acb23f60..537ca80d4320cdc5dfef82ee0b88f515083139a0 100644 (file)
@@ -6,11 +6,11 @@ import io
 import logging
 import multiprocessing
 import os
+import re
 import sys
 import types
-import unittest
 from concurrent.futures import ThreadPoolExecutor
-from contextlib import contextmanager
+from contextlib import contextmanager, redirect_stderr
 from dataclasses import replace
 from io import BytesIO
 from pathlib import Path
@@ -24,6 +24,7 @@ from typing import (
     List,
     Optional,
     Sequence,
+    Type,
     TypeVar,
     Union,
 )
@@ -31,7 +32,6 @@ from unittest.mock import MagicMock, patch
 
 import click
 import pytest
-import regex as re
 from click import unstyle
 from click.testing import CliRunner
 from pathspec import PathSpec
@@ -40,7 +40,7 @@ import black
 import black.files
 from black import Feature, TargetVersion
 from black import re_compile_maybe_verbose as compile_pattern
-from black.cache import get_cache_file
+from black.cache import FileData, get_cache_dir, get_cache_file
 from black.debug import DebugVisitor
 from black.output import color_diff, diff
 from black.report import Report
@@ -50,6 +50,7 @@ from tests.util import (
     DATA_DIR,
     DEFAULT_MODE,
     DETERMINISTIC_HEADER,
+    PROJECT_ROOT,
     PY36_VERSIONS,
     THIS_DIR,
     BlackBaseTestCase,
@@ -58,10 +59,13 @@ from tests.util import (
     dump_to_stderr,
     ff,
     fs,
+    get_case_path,
     read_data,
+    read_data_from_file,
 )
 
 THIS_FILE = Path(__file__)
+EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
@@ -69,7 +73,7 @@ T = TypeVar("T")
 R = TypeVar("R")
 
 # Match the time output in a diff, but nothing else
-DIFF_TIME = re.compile(r"\t[\d-:+\. ]+")
+DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
 
 
 @contextmanager
@@ -99,6 +103,9 @@ class FakeContext(click.Context):
 
     def __init__(self) -> None:
         self.default_map: Dict[str, Any] = {}
+        self.params: Dict[str, Any] = {}
+        # Dummy root, since most of the tests don't care about it
+        self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
 
 
 class FakeParameter(click.Parameter):
@@ -121,7 +128,7 @@ def invokeBlack(
     runner = BlackRunner()
     if ignore_config:
         args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
-    result = runner.invoke(black.main, args)
+    result = runner.invoke(black.main, args, catch_exceptions=False)
     assert result.stdout_bytes is not None
     assert result.stderr_bytes is not None
     msg = (
@@ -141,18 +148,57 @@ class BlackTestCase(BlackBaseTestCase):
         tmp_file = Path(black.dump_to_file())
         try:
             self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
-            with open(tmp_file, encoding="utf8") as f:
-                actual = f.read()
+            actual = tmp_file.read_text(encoding="utf-8")
         finally:
             os.unlink(tmp_file)
         self.assertFormatEqual(expected, actual)
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_one_empty_line(self) -> None:
+        mode = black.Mode(preview=True)
+        for nl in ["\n", "\r\n"]:
+            source = expected = nl
+            assert_format(source, expected, mode=mode)
+
+    def test_one_empty_line_ff(self) -> None:
+        mode = black.Mode(preview=True)
+        for nl in ["\n", "\r\n"]:
+            expected = nl
+            tmp_file = Path(black.dump_to_file(nl))
+            if system() == "Windows":
+                # Writing files in text mode automatically uses the system newline,
+                # but in this case we don't want this for testing reasons. See:
+                # https://github.com/psf/black/pull/3348
+                with open(tmp_file, "wb") as f:
+                    f.write(nl.encode("utf-8"))
+            try:
+                self.assertFalse(
+                    ff(tmp_file, mode=mode, write_back=black.WriteBack.YES)
+                )
+                with open(tmp_file, "rb") as f:
+                    actual = f.read().decode("utf-8")
+            finally:
+                os.unlink(tmp_file)
+            self.assertFormatEqual(expected, actual)
+
+    def test_experimental_string_processing_warns(self) -> None:
+        self.assertWarns(
+            black.mode.Deprecated, black.Mode, experimental_string_processing=True
+        )
+
     def test_piping(self) -> None:
-        source, expected = read_data("src/black/__init__", data=False)
+        _, source, expected = read_data_from_file(
+            PROJECT_ROOT / "src/black/__init__.py"
+        )
         result = BlackRunner().invoke(
             black.main,
-            ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
-            input=BytesIO(source.encode("utf8")),
+            [
+                "-",
+                "--fast",
+                f"--line-length={black.DEFAULT_LINE_LENGTH}",
+                f"--config={EMPTY_CONFIG}",
+            ],
+            input=BytesIO(source.encode("utf-8")),
         )
         self.assertEqual(result.exit_code, 0)
         self.assertFormatEqual(expected, result.output)
@@ -162,21 +208,20 @@ class BlackTestCase(BlackBaseTestCase):
 
     def test_piping_diff(self) -> None:
         diff_header = re.compile(
-            r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
-            r"\+\d\d\d\d"
+            r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d"
+            r"\+\d\d:\d\d"
         )
-        source, _ = read_data("expression.py")
-        expected, _ = read_data("expression.diff")
-        config = THIS_DIR / "data" / "empty_pyproject.toml"
+        source, _ = read_data("cases", "expression.py")
+        expected, _ = read_data("cases", "expression.diff")
         args = [
             "-",
             "--fast",
             f"--line-length={black.DEFAULT_LINE_LENGTH}",
             "--diff",
-            f"--config={config}",
+            f"--config={EMPTY_CONFIG}",
         ]
         result = BlackRunner().invoke(
-            black.main, args, input=BytesIO(source.encode("utf8"))
+            black.main, args, input=BytesIO(source.encode("utf-8"))
         )
         self.assertEqual(result.exit_code, 0)
         actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
@@ -184,22 +229,21 @@ class BlackTestCase(BlackBaseTestCase):
         self.assertEqual(expected, actual)
 
     def test_piping_diff_with_color(self) -> None:
-        source, _ = read_data("expression.py")
-        config = THIS_DIR / "data" / "empty_pyproject.toml"
+        source, _ = read_data("cases", "expression.py")
         args = [
             "-",
             "--fast",
             f"--line-length={black.DEFAULT_LINE_LENGTH}",
             "--diff",
             "--color",
-            f"--config={config}",
+            f"--config={EMPTY_CONFIG}",
         ]
         result = BlackRunner().invoke(
-            black.main, args, input=BytesIO(source.encode("utf8"))
+            black.main, args, input=BytesIO(source.encode("utf-8"))
         )
         actual = result.output
         # Again, the contents are checked in a different test, so only look for colors.
-        self.assertIn("\033[1;37m", actual)
+        self.assertIn("\033[1m", actual)
         self.assertIn("\033[36m", actual)
         self.assertIn("\033[32m", actual)
         self.assertIn("\033[31m", actual)
@@ -207,7 +251,7 @@ class BlackTestCase(BlackBaseTestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def _test_wip(self) -> None:
-        source, expected = read_data("wip")
+        source, expected = read_data("miscellaneous", "wip")
         sys.settrace(tracefunc)
         mode = replace(
             DEFAULT_MODE,
@@ -220,60 +264,29 @@ class BlackTestCase(BlackBaseTestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
-    @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_trailing_comma_optional_parens_stability1_pass2(self) -> None:
-        source, _expected = read_data("trailing_comma_optional_parens1")
-        actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
-        black.assert_stable(source, actual, DEFAULT_MODE)
-
-    @patch("black.dump_to_file", dump_to_stderr)
-    def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
-        source, _expected = read_data("trailing_comma_optional_parens2")
-        actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
-        black.assert_stable(source, actual, DEFAULT_MODE)
-
-    @patch("black.dump_to_file", dump_to_stderr)
-    def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
-        source, _expected = read_data("trailing_comma_optional_parens3")
-        actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
-        black.assert_stable(source, actual, DEFAULT_MODE)
-
     def test_pep_572_version_detection(self) -> None:
-        source, _ = read_data("pep_572")
+        source, _ = read_data("cases", "pep_572")
         root = black.lib2to3_parse(source)
         features = black.get_features_used(root)
         self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
         versions = black.detect_target_versions(root)
         self.assertIn(black.TargetVersion.PY38, versions)
 
+    def test_pep_695_version_detection(self) -> None:
+        for file in ("type_aliases", "type_params"):
+            source, _ = read_data("cases", file)
+            root = black.lib2to3_parse(source)
+            features = black.get_features_used(root)
+            self.assertIn(black.Feature.TYPE_PARAMS, features)
+            versions = black.detect_target_versions(root)
+            self.assertIn(black.TargetVersion.PY312, versions)
+
     def test_expression_ff(self) -> None:
-        source, expected = read_data("expression")
+        source, expected = read_data("cases", "expression.py")
         tmp_file = Path(black.dump_to_file(source))
         try:
             self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
-            with open(tmp_file, encoding="utf8") as f:
-                actual = f.read()
+            actual = tmp_file.read_text(encoding="utf-8")
         finally:
             os.unlink(tmp_file)
         self.assertFormatEqual(expected, actual)
@@ -282,17 +295,16 @@ class BlackTestCase(BlackBaseTestCase):
             black.assert_stable(source, actual, DEFAULT_MODE)
 
     def test_expression_diff(self) -> None:
-        source, _ = read_data("expression.py")
-        config = THIS_DIR / "data" / "empty_pyproject.toml"
-        expected, _ = read_data("expression.diff")
+        source, _ = read_data("cases", "expression.py")
+        expected, _ = read_data("cases", "expression.diff")
         tmp_file = Path(black.dump_to_file(source))
         diff_header = re.compile(
             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
-            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
         )
         try:
             result = BlackRunner().invoke(
-                black.main, ["--diff", str(tmp_file), f"--config={config}"]
+                black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
             )
             self.assertEqual(result.exit_code, 0)
         finally:
@@ -309,37 +321,57 @@ class BlackTestCase(BlackBaseTestCase):
             self.assertEqual(expected, actual, msg)
 
     def test_expression_diff_with_color(self) -> None:
-        source, _ = read_data("expression.py")
-        config = THIS_DIR / "data" / "empty_pyproject.toml"
-        expected, _ = read_data("expression.diff")
+        source, _ = read_data("cases", "expression.py")
+        expected, _ = read_data("cases", "expression.diff")
         tmp_file = Path(black.dump_to_file(source))
         try:
             result = BlackRunner().invoke(
-                black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
+                black.main,
+                ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
             )
         finally:
             os.unlink(tmp_file)
         actual = result.output
         # We check the contents of the diff in `test_expression_diff`. All
         # we need to check here is that color codes exist in the result.
-        self.assertIn("\033[1;37m", actual)
+        self.assertIn("\033[1m", actual)
         self.assertIn("\033[36m", actual)
         self.assertIn("\033[32m", actual)
         self.assertIn("\033[31m", actual)
         self.assertIn("\033[0m", actual)
 
     def test_detect_pos_only_arguments(self) -> None:
-        source, _ = read_data("pep_570")
+        source, _ = read_data("cases", "pep_570")
         root = black.lib2to3_parse(source)
         features = black.get_features_used(root)
         self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
         versions = black.detect_target_versions(root)
         self.assertIn(black.TargetVersion.PY38, versions)
 
+    def test_detect_debug_f_strings(self) -> None:
+        root = black.lib2to3_parse("""f"{x=}" """)
+        features = black.get_features_used(root)
+        self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
+        versions = black.detect_target_versions(root)
+        self.assertIn(black.TargetVersion.PY38, versions)
+
+        root = black.lib2to3_parse(
+            """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
+        )
+        features = black.get_features_used(root)
+        self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
+
+        # We don't yet support feature version detection in nested f-strings
+        root = black.lib2to3_parse(
+            """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
+        )
+        features = black.get_features_used(root)
+        self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_string_quotes(self) -> None:
-        source, expected = read_data("string_quotes")
-        mode = black.Mode(experimental_string_processing=True)
+        source, expected = read_data("miscellaneous", "string_quotes")
+        mode = black.Mode(preview=True)
         assert_format(source, expected, mode)
         mode = replace(mode, string_normalization=False)
         not_normalized = fs(source, mode=mode)
@@ -347,16 +379,43 @@ class BlackTestCase(BlackBaseTestCase):
         black.assert_equivalent(source, not_normalized)
         black.assert_stable(source, not_normalized, mode=mode)
 
+    def test_skip_source_first_line(self) -> None:
+        source, _ = read_data("miscellaneous", "invalid_header")
+        tmp_file = Path(black.dump_to_file(source))
+        # Full source should fail (invalid syntax at header)
+        self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
+        # So, skipping the first line should work
+        result = BlackRunner().invoke(
+            black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
+        )
+        self.assertEqual(result.exit_code, 0)
+        actual = tmp_file.read_text(encoding="utf-8")
+        self.assertFormatEqual(source, actual)
+
+    def test_skip_source_first_line_when_mixing_newlines(self) -> None:
+        code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
+        expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
+        with TemporaryDirectory() as workspace:
+            test_file = Path(workspace) / "skip_header.py"
+            test_file.write_bytes(code_mixing_newlines)
+            mode = replace(DEFAULT_MODE, skip_source_first_line=True)
+            ff(test_file, mode=mode, write_back=black.WriteBack.YES)
+            self.assertEqual(test_file.read_bytes(), expected)
+
     def test_skip_magic_trailing_comma(self) -> None:
-        source, _ = read_data("expression.py")
-        expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
+        source, _ = read_data("cases", "expression")
+        expected, _ = read_data(
+            "miscellaneous", "expression_skip_magic_trailing_comma.diff"
+        )
         tmp_file = Path(black.dump_to_file(source))
         diff_header = re.compile(
             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
-            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
         )
         try:
-            result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
+            result = BlackRunner().invoke(
+                black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
+            )
             self.assertEqual(result.exit_code, 0)
         finally:
             os.unlink(tmp_file)
@@ -368,14 +427,15 @@ class BlackTestCase(BlackBaseTestCase):
             msg = (
                 "Expected diff isn't equal to the actual. If you made changes to"
                 " expression.py and this is an anticipated difference, overwrite"
-                f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
+                " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
+                f" with {dump}"
             )
             self.assertEqual(expected, actual, msg)
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_async_as_identifier(self) -> None:
-        source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
-        source, expected = read_data("async_as_identifier")
+        source_path = get_case_path("miscellaneous", "async_as_identifier")
+        _, source, expected = read_data_from_file(source_path)
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         major, minor = sys.version_info[:2]
@@ -389,8 +449,8 @@ class BlackTestCase(BlackBaseTestCase):
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_python37(self) -> None:
-        source_path = (THIS_DIR / "data" / "python37.py").resolve()
-        source, expected = read_data("python37")
+        source_path = get_case_path("cases", "python37")
+        _, source, expected = read_data_from_file(source_path)
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         major, minor = sys.version_info[:2]
@@ -424,6 +484,51 @@ class BlackTestCase(BlackBaseTestCase):
         self.assertFormatEqual(contents_spc, fs(contents_spc))
         self.assertFormatEqual(contents_spc, fs(contents_tab))
 
+    def test_false_positive_symlink_output_issue_3384(self) -> None:
+        # Emulate the behavior when using the CLI (`black ./child  --verbose`), which
+        # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
+        # patched only on its first call: when checking if "./child" is a directory it
+        # should return True. The "./child" folder exists relative to the cwd when
+        # running from CLI, but fails when running the tests because cwd is different
+        project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
+        working_directory = project_root / "root"
+        target_abspath = working_directory / "child"
+        target_contents = list(target_abspath.iterdir())
+
+        def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
+            def _mocked_calls() -> bool:
+                if responses:
+                    return responses.pop(0)
+                return False
+
+            return _mocked_calls
+
+        with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
+            "pathlib.Path.cwd", return_value=working_directory
+        ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
+            # Note that the root folder (project_root) isn't the folder
+            # named "root" (aka working_directory)
+            report = MagicMock(verbose=True)
+            black.get_sources(
+                root=project_root,
+                src=("./child",),
+                quiet=False,
+                verbose=True,
+                include=DEFAULT_INCLUDE,
+                exclude=None,
+                report=report,
+                extend_exclude=None,
+                force_exclude=None,
+                stdin_filename=None,
+            )
+        assert not any(
+            mock_args[1].startswith("is a symbolic link that points outside")
+            for _, mock_args, _ in report.path_ignored.mock_calls
+        ), "A symbolic link was reported."
+        report.path_ignored.assert_called_once_with(
+            Path("root", "child", "b.py"), "matches a .gitignore file content"
+        )
+
     def test_report_verbose(self) -> None:
         report = Report(verbose=True)
         out_lines = []
@@ -515,15 +620,15 @@ class BlackTestCase(BlackBaseTestCase):
             report.check = True
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
-                " would fail to reformat.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2"
+                " files would fail to reformat.",
             )
             report.check = False
             report.diff = True
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
-                " would fail to reformat.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2"
+                " files would fail to reformat.",
             )
 
     def test_report_quiet(self) -> None:
@@ -609,15 +714,15 @@ class BlackTestCase(BlackBaseTestCase):
             report.check = True
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
-                " would fail to reformat.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2"
+                " files would fail to reformat.",
             )
             report.check = False
             report.diff = True
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
-                " would fail to reformat.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2"
+                " files would fail to reformat.",
             )
 
     def test_report_normal(self) -> None:
@@ -706,15 +811,15 @@ class BlackTestCase(BlackBaseTestCase):
             report.check = True
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
-                " would fail to reformat.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2"
+                " files would fail to reformat.",
             )
             report.check = False
             report.diff = True
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
-                " would fail to reformat.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2"
+                " files would fail to reformat.",
             )
 
     def test_lib2to3_parse(self) -> None:
@@ -723,31 +828,22 @@ class BlackTestCase(BlackBaseTestCase):
 
         straddling = "x + y"
         black.lib2to3_parse(straddling)
-        black.lib2to3_parse(straddling, {TargetVersion.PY27})
         black.lib2to3_parse(straddling, {TargetVersion.PY36})
-        black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
 
         py2_only = "print x"
-        black.lib2to3_parse(py2_only)
-        black.lib2to3_parse(py2_only, {TargetVersion.PY27})
         with self.assertRaises(black.InvalidInput):
             black.lib2to3_parse(py2_only, {TargetVersion.PY36})
-        with self.assertRaises(black.InvalidInput):
-            black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
 
         py3_only = "exec(x, end=y)"
         black.lib2to3_parse(py3_only)
-        with self.assertRaises(black.InvalidInput):
-            black.lib2to3_parse(py3_only, {TargetVersion.PY27})
         black.lib2to3_parse(py3_only, {TargetVersion.PY36})
-        black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
 
     def test_get_features_used_decorator(self) -> None:
         # Test the feature detection of new decorator syntax
         # since this makes some test cases of test_get_features_used()
         # fails if it fails, this is tested first so that a useful case
         # is identified
-        simples, relaxed = read_data("decorators")
+        simples, relaxed = read_data("miscellaneous", "decorators")
         # skip explanation comments at the top of the file
         for simple_test in simples.split("##")[1:]:
             node = black.lib2to3_parse(simple_test)
@@ -790,7 +886,7 @@ class BlackTestCase(BlackBaseTestCase):
         self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
         node = black.lib2to3_parse("123456\n")
         self.assertEqual(black.get_features_used(node), set())
-        source, expected = read_data("function")
+        source, expected = read_data("cases", "function")
         node = black.lib2to3_parse(source)
         expected_features = {
             Feature.TRAILING_COMMA_IN_CALL,
@@ -800,7 +896,7 @@ class BlackTestCase(BlackBaseTestCase):
         self.assertEqual(black.get_features_used(node), expected_features)
         node = black.lib2to3_parse(expected)
         self.assertEqual(black.get_features_used(node), expected_features)
-        source, expected = read_data("expression")
+        source, expected = read_data("cases", "expression")
         node = black.lib2to3_parse(source)
         self.assertEqual(black.get_features_used(node), set())
         node = black.lib2to3_parse(expected)
@@ -809,6 +905,56 @@ class BlackTestCase(BlackBaseTestCase):
         self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
         node = black.lib2to3_parse("def fn(a, /, b): ...")
         self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
+        node = black.lib2to3_parse("def fn(): yield a, b")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("def fn(): return a, b")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("def fn(): yield *b, c")
+        self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
+        node = black.lib2to3_parse("def fn(): return a, *b, c")
+        self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
+        node = black.lib2to3_parse("x = a, *b, c")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("x: Any = regular")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("x: Any = (regular, regular)")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
+        self.assertEqual(
+            black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
+        )
+        node = black.lib2to3_parse("try: pass\nexcept Something: pass")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
+        self.assertEqual(black.get_features_used(node), set())
+        node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
+        self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
+        node = black.lib2to3_parse("a[*b]")
+        self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
+        node = black.lib2to3_parse("a[x, *y(), z] = t")
+        self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
+        node = black.lib2to3_parse("def fn(*args: *T): pass")
+        self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
+
+    def test_get_features_used_for_future_flags(self) -> None:
+        for src, features in [
+            ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
+            (
+                "from __future__ import (other, annotations)",
+                {Feature.FUTURE_ANNOTATIONS},
+            ),
+            ("a = 1 + 2\nfrom something import annotations", set()),
+            ("from __future__ import x, y", set()),
+        ]:
+            with self.subTest(src=src, features=features):
+                node = black.lib2to3_parse(src)
+                future_imports = black.get_future_imports(node)
+                self.assertEqual(
+                    black.get_features_used(node, future_imports=future_imports),
+                    features,
+                )
 
     def test_get_future_imports(self) -> None:
         node = black.lib2to3_parse("\n")
@@ -840,9 +986,10 @@ class BlackTestCase(BlackBaseTestCase):
         )
         self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
 
+    @pytest.mark.incompatible_with_mypyc
     def test_debug_visitor(self) -> None:
-        source, _ = read_data("debug_visitor.py")
-        expected, _ = read_data("debug_visitor.out")
+        source, _ = read_data("miscellaneous", "debug_visitor")
+        expected, _ = read_data("miscellaneous", "debug_visitor.out")
         out_lines = []
         err_lines = []
 
@@ -865,8 +1012,8 @@ class BlackTestCase(BlackBaseTestCase):
         )
 
     def test_format_file_contents(self) -> None:
-        empty = ""
         mode = DEFAULT_MODE
+        empty = ""
         with self.assertRaises(black.NothingChanged):
             black.format_file_contents(empty, mode=mode, fast=False)
         just_nl = "\n"
@@ -884,14 +1031,27 @@ class BlackTestCase(BlackBaseTestCase):
             black.format_file_contents(invalid, mode=mode, fast=False)
         self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
 
+        mode = black.Mode(preview=True)
+        just_crlf = "\r\n"
+        with self.assertRaises(black.NothingChanged):
+            black.format_file_contents(just_crlf, mode=mode, fast=False)
+        just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
+        actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
+        self.assertEqual("\n", actual)
+        just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
+        actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
+        self.assertEqual("\r\n", actual)
+
     def test_endmarker(self) -> None:
         n = black.lib2to3_parse("\n")
         self.assertEqual(n.type, black.syms.file_input)
         self.assertEqual(len(n.children), 1)
         self.assertEqual(n.children[0].type, black.token.ENDMARKER)
 
-    @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
-    def test_assertFormatEqual(self) -> None:
+    @patch("tests.conftest.PRINT_FULL_TREE", True)
+    @patch("tests.conftest.PRINT_TREE_DIFF", False)
+    @pytest.mark.incompatible_with_mypyc
+    def test_assertFormatEqual_print_full_tree(self) -> None:
         out_lines = []
         err_lines = []
 
@@ -906,62 +1066,86 @@ class BlackTestCase(BlackBaseTestCase):
                 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
 
         out_str = "".join(out_lines)
-        self.assertTrue("Expected tree:" in out_str)
-        self.assertTrue("Actual tree:" in out_str)
+        self.assertIn("Expected tree:", out_str)
+        self.assertIn("Actual tree:", out_str)
+        self.assertEqual("".join(err_lines), "")
+
+    @patch("tests.conftest.PRINT_FULL_TREE", False)
+    @patch("tests.conftest.PRINT_TREE_DIFF", True)
+    @pytest.mark.incompatible_with_mypyc
+    def test_assertFormatEqual_print_tree_diff(self) -> None:
+        out_lines = []
+        err_lines = []
+
+        def out(msg: str, **kwargs: Any) -> None:
+            out_lines.append(msg)
+
+        def err(msg: str, **kwargs: Any) -> None:
+            err_lines.append(msg)
+
+        with patch("black.output._out", out), patch("black.output._err", err):
+            with self.assertRaises(AssertionError):
+                self.assertFormatEqual("j = [1, 2, 3]\n", "j = [1, 2, 3,]\n")
+
+        out_str = "".join(out_lines)
+        self.assertIn("Tree Diff:", out_str)
+        self.assertIn("+          COMMA", out_str)
+        self.assertIn("+ ','", out_str)
         self.assertEqual("".join(err_lines), "")
 
     @event_loop()
-    @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
+    @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
     def test_works_in_mono_process_only_environment(self) -> None:
         with cache_dir() as workspace:
             for f in [
                 (workspace / "one.py").resolve(),
                 (workspace / "two.py").resolve(),
             ]:
-                f.write_text('print("hello")\n')
+                f.write_text('print("hello")\n', encoding="utf-8")
             self.invokeBlack([str(workspace)])
 
     @event_loop()
     def test_check_diff_use_together(self) -> None:
         with cache_dir():
             # Files which will be reformatted.
-            src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
+            src1 = get_case_path("miscellaneous", "string_quotes")
             self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
             # Files which will not be reformatted.
-            src2 = (THIS_DIR / "data" / "composition.py").resolve()
+            src2 = get_case_path("cases", "composition")
             self.invokeBlack([str(src2), "--diff", "--check"])
             # Multi file command.
             self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
 
-    def test_no_files(self) -> None:
+    def test_no_src_fails(self) -> None:
+        with cache_dir():
+            self.invokeBlack([], exit_code=1)
+
+    def test_src_and_code_fails(self) -> None:
         with cache_dir():
-            # Without an argument, black exits with error code 0.
-            self.invokeBlack([])
+            self.invokeBlack([".", "-c", "0"], exit_code=1)
 
     def test_broken_symlink(self) -> None:
         with cache_dir() as workspace:
             symlink = workspace / "broken_link.py"
             try:
                 symlink.symlink_to("nonexistent.py")
-            except OSError as e:
+            except (OSError, NotImplementedError) as e:
                 self.skipTest(f"Can't create symlinks: {e}")
             self.invokeBlack([str(workspace.resolve())])
 
     def test_single_file_force_pyi(self) -> None:
         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
-        contents, expected = read_data("force_pyi")
+        contents, expected = read_data("miscellaneous", "force_pyi")
         with cache_dir() as workspace:
             path = (workspace / "file.py").resolve()
-            with open(path, "w") as fh:
-                fh.write(contents)
+            path.write_text(contents, encoding="utf-8")
             self.invokeBlack([str(path), "--pyi"])
-            with open(path, "r") as fh:
-                actual = fh.read()
+            actual = path.read_text(encoding="utf-8")
             # verify cache with --pyi is separate
-            pyi_cache = black.read_cache(pyi_mode)
-            self.assertIn(str(path), pyi_cache)
-            normal_cache = black.read_cache(DEFAULT_MODE)
-            self.assertNotIn(str(path), normal_cache)
+            pyi_cache = black.Cache.read(pyi_mode)
+            assert not pyi_cache.is_changed(path)
+            normal_cache = black.Cache.read(DEFAULT_MODE)
+            assert normal_cache.is_changed(path)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(contents, actual)
         black.assert_stable(contents, actual, pyi_mode)
@@ -970,31 +1154,29 @@ class BlackTestCase(BlackBaseTestCase):
     def test_multi_file_force_pyi(self) -> None:
         reg_mode = DEFAULT_MODE
         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
-        contents, expected = read_data("force_pyi")
+        contents, expected = read_data("miscellaneous", "force_pyi")
         with cache_dir() as workspace:
             paths = [
                 (workspace / "file1.py").resolve(),
                 (workspace / "file2.py").resolve(),
             ]
             for path in paths:
-                with open(path, "w") as fh:
-                    fh.write(contents)
+                path.write_text(contents, encoding="utf-8")
             self.invokeBlack([str(p) for p in paths] + ["--pyi"])
             for path in paths:
-                with open(path, "r") as fh:
-                    actual = fh.read()
+                actual = path.read_text(encoding="utf-8")
                 self.assertEqual(actual, expected)
             # verify cache with --pyi is separate
-            pyi_cache = black.read_cache(pyi_mode)
-            normal_cache = black.read_cache(reg_mode)
+            pyi_cache = black.Cache.read(pyi_mode)
+            normal_cache = black.Cache.read(reg_mode)
             for path in paths:
-                self.assertIn(str(path), pyi_cache)
-                self.assertNotIn(str(path), normal_cache)
+                assert not pyi_cache.is_changed(path)
+                assert normal_cache.is_changed(path)
 
     def test_pipe_force_pyi(self) -> None:
-        source, expected = read_data("force_pyi")
+        source, expected = read_data("miscellaneous", "force_pyi")
         result = CliRunner().invoke(
-            black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
+            black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf-8"))
         )
         self.assertEqual(result.exit_code, 0)
         actual = result.output
@@ -1003,57 +1185,54 @@ class BlackTestCase(BlackBaseTestCase):
     def test_single_file_force_py36(self) -> None:
         reg_mode = DEFAULT_MODE
         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
-        source, expected = read_data("force_py36")
+        source, expected = read_data("miscellaneous", "force_py36")
         with cache_dir() as workspace:
             path = (workspace / "file.py").resolve()
-            with open(path, "w") as fh:
-                fh.write(source)
+            path.write_text(source, encoding="utf-8")
             self.invokeBlack([str(path), *PY36_ARGS])
-            with open(path, "r") as fh:
-                actual = fh.read()
+            actual = path.read_text(encoding="utf-8")
             # verify cache with --target-version is separate
-            py36_cache = black.read_cache(py36_mode)
-            self.assertIn(str(path), py36_cache)
-            normal_cache = black.read_cache(reg_mode)
-            self.assertNotIn(str(path), normal_cache)
+            py36_cache = black.Cache.read(py36_mode)
+            assert not py36_cache.is_changed(path)
+            normal_cache = black.Cache.read(reg_mode)
+            assert normal_cache.is_changed(path)
         self.assertEqual(actual, expected)
 
     @event_loop()
     def test_multi_file_force_py36(self) -> None:
         reg_mode = DEFAULT_MODE
         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
-        source, expected = read_data("force_py36")
+        source, expected = read_data("miscellaneous", "force_py36")
         with cache_dir() as workspace:
             paths = [
                 (workspace / "file1.py").resolve(),
                 (workspace / "file2.py").resolve(),
             ]
             for path in paths:
-                with open(path, "w") as fh:
-                    fh.write(source)
+                path.write_text(source, encoding="utf-8")
             self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
             for path in paths:
-                with open(path, "r") as fh:
-                    actual = fh.read()
+                actual = path.read_text(encoding="utf-8")
                 self.assertEqual(actual, expected)
             # verify cache with --target-version is separate
-            pyi_cache = black.read_cache(py36_mode)
-            normal_cache = black.read_cache(reg_mode)
+            pyi_cache = black.Cache.read(py36_mode)
+            normal_cache = black.Cache.read(reg_mode)
             for path in paths:
-                self.assertIn(str(path), pyi_cache)
-                self.assertNotIn(str(path), normal_cache)
+                assert not pyi_cache.is_changed(path)
+                assert normal_cache.is_changed(path)
 
     def test_pipe_force_py36(self) -> None:
-        source, expected = read_data("force_py36")
+        source, expected = read_data("miscellaneous", "force_py36")
         result = CliRunner().invoke(
             black.main,
             ["-", "-q", "--target-version=py36"],
-            input=BytesIO(source.encode("utf8")),
+            input=BytesIO(source.encode("utf-8")),
         )
         self.assertEqual(result.exit_code, 0)
         actual = result.output
         self.assertFormatEqual(actual, expected)
 
+    @pytest.mark.incompatible_with_mypyc
     def test_reformat_one_with_stdin(self) -> None:
         with patch(
             "black.format_stdin_to_stdout",
@@ -1071,6 +1250,7 @@ class BlackTestCase(BlackBaseTestCase):
             fsts.assert_called_once()
             report.done.assert_called_with(path, black.Changed.YES)
 
+    @pytest.mark.incompatible_with_mypyc
     def test_reformat_one_with_stdin_filename(self) -> None:
         with patch(
             "black.format_stdin_to_stdout",
@@ -1093,6 +1273,7 @@ class BlackTestCase(BlackBaseTestCase):
             # __BLACK_STDIN_FILENAME__ should have been stripped
             report.done.assert_called_with(expected, black.Changed.YES)
 
+    @pytest.mark.incompatible_with_mypyc
     def test_reformat_one_with_stdin_filename_pyi(self) -> None:
         with patch(
             "black.format_stdin_to_stdout",
@@ -1117,6 +1298,7 @@ class BlackTestCase(BlackBaseTestCase):
             # __BLACK_STDIN_FILENAME__ should have been stripped
             report.done.assert_called_with(expected, black.Changed.YES)
 
+    @pytest.mark.incompatible_with_mypyc
     def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
         with patch(
             "black.format_stdin_to_stdout",
@@ -1141,6 +1323,7 @@ class BlackTestCase(BlackBaseTestCase):
             # __BLACK_STDIN_FILENAME__ should have been stripped
             report.done.assert_called_with(expected, black.Changed.YES)
 
+    @pytest.mark.incompatible_with_mypyc
     def test_reformat_one_with_stdin_and_existing_path(self) -> None:
         with patch(
             "black.format_stdin_to_stdout",
@@ -1149,7 +1332,7 @@ class BlackTestCase(BlackBaseTestCase):
             report = MagicMock()
             # Even with an existing file, since we are forcing stdin, black
             # should output to stdout and not modify the file inplace
-            p = Path(str(THIS_DIR / "data/collections.py"))
+            p = THIS_DIR / "data" / "cases" / "collections.py"
             # Make sure is_file actually returns True
             self.assertTrue(p.is_file())
             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
@@ -1166,8 +1349,51 @@ class BlackTestCase(BlackBaseTestCase):
             report.done.assert_called_with(expected, black.Changed.YES)
 
     def test_reformat_one_with_stdin_empty(self) -> None:
+        cases = [
+            ("", ""),
+            ("\n", "\n"),
+            ("\r\n", "\r\n"),
+            (" \t", ""),
+            (" \t\n\t ", "\n"),
+            (" \t\r\n\t ", "\r\n"),
+        ]
+
+        def _new_wrapper(
+            output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
+        ) -> Callable[[Any, Any], io.TextIOWrapper]:
+            def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
+                if args == (sys.stdout.buffer,):
+                    # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
+                    # return our mock object.
+                    return output
+                # It's something else (i.e. `decode_bytes()`) calling
+                # `io.TextIOWrapper()`, pass through to the original implementation.
+                # See discussion in https://github.com/psf/black/pull/2489
+                return io_TextIOWrapper(*args, **kwargs)
+
+            return get_output
+
+        mode = black.Mode(preview=True)
+        for content, expected in cases:
+            output = io.StringIO()
+            io_TextIOWrapper = io.TextIOWrapper
+
+            with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
+                try:
+                    black.format_stdin_to_stdout(
+                        fast=True,
+                        content=content,
+                        write_back=black.WriteBack.YES,
+                        mode=mode,
+                    )
+                except io.UnsupportedOperation:
+                    pass  # StringIO does not support detach
+                assert output.getvalue() == expected
+
+        # An empty string is the only test case for `preview=False`
         output = io.StringIO()
-        with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
+        io_TextIOWrapper = io.TextIOWrapper
+        with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
             try:
                 black.format_stdin_to_stdout(
                     fast=True,
@@ -1185,13 +1411,32 @@ class BlackTestCase(BlackBaseTestCase):
 
     def test_required_version_matches_version(self) -> None:
         self.invokeBlack(
-            ["--required-version", black.__version__], exit_code=0, ignore_config=True
+            ["--required-version", black.__version__, "-c", "0"],
+            exit_code=0,
+            ignore_config=True,
         )
 
-    def test_required_version_does_not_match_version(self) -> None:
+    def test_required_version_matches_partial_version(self) -> None:
         self.invokeBlack(
-            ["--required-version", "20.99b"], exit_code=1, ignore_config=True
+            ["--required-version", black.__version__.split(".")[0], "-c", "0"],
+            exit_code=0,
+            ignore_config=True,
+        )
+
+    def test_required_version_does_not_match_on_minor_version(self) -> None:
+        self.invokeBlack(
+            ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
+            exit_code=1,
+            ignore_config=True,
+        )
+
+    def test_required_version_does_not_match_version(self) -> None:
+        result = BlackRunner().invoke(
+            black.main,
+            ["--required-version", "20.99b", "-c", "0"],
         )
+        self.assertEqual(result.exit_code, 1)
+        self.assertIn("required version", result.stderr)
 
     def test_preserves_line_endings(self) -> None:
         with TemporaryDirectory() as workspace:
@@ -1210,40 +1455,29 @@ class BlackTestCase(BlackBaseTestCase):
             contents = nl.join(["def f(  ):", "    pass"])
             runner = BlackRunner()
             result = runner.invoke(
-                black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
+                black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf-8"))
             )
             self.assertEqual(result.exit_code, 0)
             output = result.stdout_bytes
-            self.assertIn(nl.encode("utf8"), output)
+            self.assertIn(nl.encode("utf-8"), output)
             if nl == "\n":
                 self.assertNotIn(b"\r\n", output)
 
+    def test_normalize_line_endings(self) -> None:
+        with TemporaryDirectory() as workspace:
+            test_file = Path(workspace) / "test.py"
+            for data, expected in (
+                (b"c\r\nc\n ", b"c\r\nc\r\n"),
+                (b"l\nl\r\n ", b"l\nl\n"),
+            ):
+                test_file.write_bytes(data)
+                ff(test_file, write_back=black.WriteBack.YES)
+                self.assertEqual(test_file.read_bytes(), expected)
+
     def test_assert_equivalent_different_asts(self) -> None:
         with self.assertRaises(AssertionError):
             black.assert_equivalent("{}", "None")
 
-    def test_shhh_click(self) -> None:
-        try:
-            from click import _unicodefun
-        except ModuleNotFoundError:
-            self.skipTest("Incompatible Click version")
-        if not hasattr(_unicodefun, "_verify_python3_env"):
-            self.skipTest("Incompatible Click version")
-        # First, let's see if Click is crashing with a preferred ASCII charset.
-        with patch("locale.getpreferredencoding") as gpe:
-            gpe.return_value = "ASCII"
-            with self.assertRaises(RuntimeError):
-                _unicodefun._verify_python3_env()  # type: ignore
-        # Now, let's silence Click...
-        black.patch_click()
-        # ...and confirm it's silent.
-        with patch("locale.getpreferredencoding") as gpe:
-            gpe.return_value = "ASCII"
-            try:
-                _unicodefun._verify_python3_env()  # type: ignore
-            except RuntimeError as re:
-                self.fail(f"`patch_click()` failed, exception still raised: {re}")
-
     def test_root_logger_not_used_directly(self) -> None:
         def fail(*args: Any, **kwargs: Any) -> None:
             self.fail("Record created with root logger")
@@ -1278,9 +1512,89 @@ class BlackTestCase(BlackBaseTestCase):
         self.assertEqual(config["color"], True)
         self.assertEqual(config["line_length"], 79)
         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
+        self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
         self.assertEqual(config["exclude"], r"\.pyi?$")
         self.assertEqual(config["include"], r"\.py?$")
 
+    def test_parse_pyproject_toml_project_metadata(self) -> None:
+        for test_toml, expected in [
+            ("only_black_pyproject.toml", ["py310"]),
+            ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
+            ("neither_pyproject.toml", None),
+            ("both_pyproject.toml", ["py310"]),
+        ]:
+            test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
+            config = black.parse_pyproject_toml(str(test_toml_file))
+            self.assertEqual(config.get("target_version"), expected)
+
+    def test_infer_target_version(self) -> None:
+        for version, expected in [
+            ("3.6", [TargetVersion.PY36]),
+            ("3.11.0rc1", [TargetVersion.PY311]),
+            (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
+            (
+                ">=3.10.6",
+                [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
+            ),
+            ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
+            (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
+            (
+                ">3.7,!=3.8,!=3.9",
+                [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
+            ),
+            (
+                "> 3.9.4, != 3.10.3",
+                [
+                    TargetVersion.PY39,
+                    TargetVersion.PY310,
+                    TargetVersion.PY311,
+                    TargetVersion.PY312,
+                ],
+            ),
+            (
+                "!=3.3,!=3.4",
+                [
+                    TargetVersion.PY35,
+                    TargetVersion.PY36,
+                    TargetVersion.PY37,
+                    TargetVersion.PY38,
+                    TargetVersion.PY39,
+                    TargetVersion.PY310,
+                    TargetVersion.PY311,
+                    TargetVersion.PY312,
+                ],
+            ),
+            (
+                "==3.*",
+                [
+                    TargetVersion.PY33,
+                    TargetVersion.PY34,
+                    TargetVersion.PY35,
+                    TargetVersion.PY36,
+                    TargetVersion.PY37,
+                    TargetVersion.PY38,
+                    TargetVersion.PY39,
+                    TargetVersion.PY310,
+                    TargetVersion.PY311,
+                    TargetVersion.PY312,
+                ],
+            ),
+            ("==3.8.*", [TargetVersion.PY38]),
+            (None, None),
+            ("", None),
+            ("invalid", None),
+            ("==invalid", None),
+            (">3.9,!=invalid", None),
+            ("3", None),
+            ("3.2", None),
+            ("2.7.18", None),
+            ("==2.7", None),
+            (">3.10,<3.11", None),
+        ]:
+            test_toml = {"project": {"requires-python": version}}
+            result = black.files.infer_target_version(test_toml)
+            self.assertEqual(result, expected)
+
     def test_read_pyproject_toml(self) -> None:
         test_toml_file = THIS_DIR / "test.toml"
         fake_ctx = FakeContext()
@@ -1295,6 +1609,40 @@ class BlackTestCase(BlackBaseTestCase):
         self.assertEqual(config["exclude"], r"\.pyi?$")
         self.assertEqual(config["include"], r"\.py?$")
 
+    def test_read_pyproject_toml_from_stdin(self) -> None:
+        with TemporaryDirectory() as workspace:
+            root = Path(workspace)
+
+            src_dir = root / "src"
+            src_dir.mkdir()
+
+            src_pyproject = src_dir / "pyproject.toml"
+            src_pyproject.touch()
+
+            test_toml_content = (THIS_DIR / "test.toml").read_text(encoding="utf-8")
+            src_pyproject.write_text(test_toml_content, encoding="utf-8")
+
+            src_python = src_dir / "foo.py"
+            src_python.touch()
+
+            fake_ctx = FakeContext()
+            fake_ctx.params["src"] = ("-",)
+            fake_ctx.params["stdin_filename"] = str(src_python)
+
+            with change_directory(root):
+                black.read_pyproject_toml(fake_ctx, FakeParameter(), None)
+
+            config = fake_ctx.default_map
+            self.assertEqual(config["verbose"], "1")
+            self.assertEqual(config["check"], "no")
+            self.assertEqual(config["diff"], "y")
+            self.assertEqual(config["color"], "True")
+            self.assertEqual(config["line_length"], "79")
+            self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
+            self.assertEqual(config["exclude"], r"\.pyi?$")
+            self.assertEqual(config["include"], r"\.py?$")
+
+    @pytest.mark.incompatible_with_mypyc
     def test_find_project_root(self) -> None:
         with TemporaryDirectory() as workspace:
             root = Path(workspace)
@@ -1312,10 +1660,38 @@ class BlackTestCase(BlackBaseTestCase):
             src_python.touch()
 
             self.assertEqual(
-                black.find_project_root((src_dir, test_dir)), root.resolve()
+                black.find_project_root((src_dir, test_dir)),
+                (root.resolve(), "pyproject.toml"),
+            )
+            self.assertEqual(
+                black.find_project_root((src_dir,)),
+                (src_dir.resolve(), "pyproject.toml"),
+            )
+            self.assertEqual(
+                black.find_project_root((src_python,)),
+                (src_dir.resolve(), "pyproject.toml"),
             )
-            self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
-            self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
+
+            with change_directory(test_dir):
+                self.assertEqual(
+                    black.find_project_root(("-",), stdin_filename="../src/a.py"),
+                    (src_dir.resolve(), "pyproject.toml"),
+                )
+
+    @patch(
+        "black.files.find_user_pyproject_toml",
+    )
+    def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
+        find_user_pyproject_toml.side_effect = RuntimeError()
+
+        with redirect_stderr(io.StringIO()) as stderr:
+            result = black.files.find_pyproject_toml(
+                path_search_start=(str(Path.cwd().root),)
+            )
+
+        assert result is None
+        err = stderr.getvalue()
+        assert "Ignoring user configuration" in err
 
     @patch(
         "black.files.find_user_pyproject_toml",
@@ -1362,23 +1738,41 @@ class BlackTestCase(BlackBaseTestCase):
             normalized_path = black.normalize_path_maybe_ignore(path, root, report)
             self.assertEqual(normalized_path, "workspace/project")
 
+    def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
+        if system() != "Windows":
+            return
+
+        with TemporaryDirectory() as workspace:
+            root = Path(workspace)
+            junction_dir = root / "junction"
+            junction_target_outside_of_root = root / ".."
+            os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
+
+            report = black.Report(verbose=True)
+            normalized_path = black.normalize_path_maybe_ignore(
+                junction_dir, root, report
+            )
+            # Manually delete for Python < 3.8
+            os.system(f"rmdir {junction_dir}")
+
+            self.assertEqual(normalized_path, None)
+
     def test_newline_comment_interaction(self) -> None:
         source = "class A:\\\r\n# type: ignore\n pass\n"
         output = black.format_str(source, mode=DEFAULT_MODE)
         black.assert_stable(source, output, mode=DEFAULT_MODE)
 
     def test_bpo_2142_workaround(self) -> None:
-
         # https://bugs.python.org/issue2142
 
-        source, _ = read_data("missing_final_newline.py")
+        source, _ = read_data("miscellaneous", "missing_final_newline")
         # read_data adds a trailing newline
         source = source.rstrip()
-        expected, _ = read_data("missing_final_newline.diff")
+        expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
         tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
         diff_header = re.compile(
             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
-            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
         )
         try:
             result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
@@ -1389,27 +1783,6 @@ class BlackTestCase(BlackBaseTestCase):
         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
         self.assertEqual(actual, expected)
 
-    @pytest.mark.python2
-    def test_docstring_reformat_for_py27(self) -> None:
-        """
-        Check that stripping trailing whitespace from Python 2 docstrings
-        doesn't trigger a "not equivalent to source" error
-        """
-        source = (
-            b'def foo():\r\n    """Testing\r\n    Testing """\r\n    print "Foo"\r\n'
-        )
-        expected = 'def foo():\n    """Testing\n    Testing"""\n    print "Foo"\n'
-
-        result = CliRunner().invoke(
-            black.main,
-            ["-", "-q", "--target-version=py27"],
-            input=BytesIO(source),
-        )
-
-        self.assertEqual(result.exit_code, 0)
-        actual = result.output
-        self.assertFormatEqual(actual, expected)
-
     @staticmethod
     def compare_results(
         result: click.testing.Result, expected_value: str, expected_exit_code: int
@@ -1482,6 +1855,7 @@ class BlackTestCase(BlackBaseTestCase):
         assert output == result_diff, "The output did not match the expected value."
         assert result.exit_code == 0, "The exit code is incorrect."
 
+    @pytest.mark.incompatible_with_mypyc
     def test_code_option_safe(self) -> None:
         """Test that the code option throws an error when the sanity checks fail."""
         # Patch black.assert_equivalent to ensure the sanity checks fail
@@ -1506,15 +1880,18 @@ class BlackTestCase(BlackBaseTestCase):
 
             self.compare_results(result, formatted, 0)
 
+    @pytest.mark.incompatible_with_mypyc
     def test_code_option_config(self) -> None:
         """
         Test that the code option finds the pyproject.toml in the current directory.
         """
         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
             args = ["--code", "print"]
-            CliRunner().invoke(black.main, args)
+            # This is the only directory known to contain a pyproject.toml
+            with change_directory(PROJECT_ROOT):
+                CliRunner().invoke(black.main, args)
+                pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
 
-            pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve()
             assert (
                 len(parse.mock_calls) >= 1
             ), "Expected config parse to be called with the current directory."
@@ -1524,12 +1901,13 @@ class BlackTestCase(BlackBaseTestCase):
                 call_args[0].lower() == str(pyproject_path).lower()
             ), "Incorrect config loaded."
 
+    @pytest.mark.incompatible_with_mypyc
     def test_code_option_parent_config(self) -> None:
         """
         Test that the code option finds the pyproject.toml in the parent directory.
         """
         with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
-            with change_directory(Path("tests")):
+            with change_directory(THIS_DIR):
                 args = ["--code", "print"]
                 CliRunner().invoke(black.main, args)
 
@@ -1543,60 +1921,104 @@ class BlackTestCase(BlackBaseTestCase):
                     call_args[0].lower() == str(pyproject_path).lower()
                 ), "Incorrect config loaded."
 
+    def test_for_handled_unexpected_eof_error(self) -> None:
+        """
+        Test that an unexpected EOF SyntaxError is nicely presented.
+        """
+        with pytest.raises(black.parsing.InvalidInput) as exc_info:
+            black.lib2to3_parse("print(", {})
+
+        exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
+
+    def test_equivalency_ast_parse_failure_includes_error(self) -> None:
+        with pytest.raises(AssertionError) as err:
+            black.assert_equivalent("a«»a  = 1", "a«»a  = 1")
+
+        err.match("--safe")
+        # Unfortunately the SyntaxError message has changed in newer versions so we
+        # can't match it directly.
+        err.match("invalid character")
+        err.match(r"\(<unknown>, line 1\)")
+
 
 class TestCaching:
+    def test_get_cache_dir(
+        self,
+        tmp_path: Path,
+        monkeypatch: pytest.MonkeyPatch,
+    ) -> None:
+        # Create multiple cache directories
+        workspace1 = tmp_path / "ws1"
+        workspace1.mkdir()
+        workspace2 = tmp_path / "ws2"
+        workspace2.mkdir()
+
+        # Force user_cache_dir to use the temporary directory for easier assertions
+        patch_user_cache_dir = patch(
+            target="black.cache.user_cache_dir",
+            autospec=True,
+            return_value=str(workspace1),
+        )
+
+        # If BLACK_CACHE_DIR is not set, use user_cache_dir
+        monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
+        with patch_user_cache_dir:
+            assert get_cache_dir().parent == workspace1
+
+        # If it is set, use the path provided in the env var.
+        monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
+        assert get_cache_dir().parent == workspace2
+
     def test_cache_broken_file(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir() as workspace:
             cache_file = get_cache_file(mode)
-            cache_file.write_text("this is not a pickle")
-            assert black.read_cache(mode) == {}
+            cache_file.write_text("this is not a pickle", encoding="utf-8")
+            assert black.Cache.read(mode).file_data == {}
             src = (workspace / "test.py").resolve()
-            src.write_text("print('hello')")
+            src.write_text("print('hello')", encoding="utf-8")
             invokeBlack([str(src)])
-            cache = black.read_cache(mode)
-            assert str(src) in cache
+            cache = black.Cache.read(mode)
+            assert not cache.is_changed(src)
 
     def test_cache_single_file_already_cached(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir() as workspace:
             src = (workspace / "test.py").resolve()
-            src.write_text("print('hello')")
-            black.write_cache({}, [src], mode)
+            src.write_text("print('hello')", encoding="utf-8")
+            cache = black.Cache.read(mode)
+            cache.write([src])
             invokeBlack([str(src)])
-            assert src.read_text() == "print('hello')"
+            assert src.read_text(encoding="utf-8") == "print('hello')"
 
     @event_loop()
     def test_cache_multiple_files(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir() as workspace, patch(
-            "black.ProcessPoolExecutor", new=ThreadPoolExecutor
+            "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
         ):
             one = (workspace / "one.py").resolve()
-            with one.open("w") as fobj:
-                fobj.write("print('hello')")
+            one.write_text("print('hello')", encoding="utf-8")
             two = (workspace / "two.py").resolve()
-            with two.open("w") as fobj:
-                fobj.write("print('hello')")
-            black.write_cache({}, [one], mode)
+            two.write_text("print('hello')", encoding="utf-8")
+            cache = black.Cache.read(mode)
+            cache.write([one])
             invokeBlack([str(workspace)])
-            with one.open("r") as fobj:
-                assert fobj.read() == "print('hello')"
-            with two.open("r") as fobj:
-                assert fobj.read() == 'print("hello")\n'
-            cache = black.read_cache(mode)
-            assert str(one) in cache
-            assert str(two) in cache
+            assert one.read_text(encoding="utf-8") == "print('hello')"
+            assert two.read_text(encoding="utf-8") == 'print("hello")\n'
+            cache = black.Cache.read(mode)
+            assert not cache.is_changed(one)
+            assert not cache.is_changed(two)
 
+    @pytest.mark.incompatible_with_mypyc
     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
     def test_no_cache_when_writeback_diff(self, color: bool) -> None:
         mode = DEFAULT_MODE
         with cache_dir() as workspace:
             src = (workspace / "test.py").resolve()
-            with src.open("w") as fobj:
-                fobj.write("print('hello')")
-            with patch("black.read_cache") as read_cache, patch(
-                "black.write_cache"
+            src.write_text("print('hello')", encoding="utf-8")
+            with patch.object(black.Cache, "read") as read_cache, patch.object(
+                black.Cache, "write"
             ) as write_cache:
                 cmd = [str(src), "--diff"]
                 if color:
@@ -1604,8 +2026,8 @@ class TestCaching:
                 invokeBlack(cmd)
                 cache_file = get_cache_file(mode)
                 assert cache_file.exists() is False
+                read_cache.assert_called_once()
                 write_cache.assert_not_called()
-                read_cache.assert_not_called()
 
     @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
     @event_loop()
@@ -1613,9 +2035,10 @@ class TestCaching:
         with cache_dir() as workspace:
             for tag in range(0, 4):
                 src = (workspace / f"test{tag}.py").resolve()
-                with src.open("w") as fobj:
-                    fobj.write("print('hello')")
-            with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
+                src.write_text("print('hello')", encoding="utf-8")
+            with patch(
+                "black.concurrency.Manager", wraps=multiprocessing.Manager
+            ) as mgr:
                 cmd = ["--diff", str(workspace)]
                 if color:
                     cmd.append("--color")
@@ -1637,18 +2060,19 @@ class TestCaching:
     def test_read_cache_no_cachefile(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir():
-            assert black.read_cache(mode) == {}
+            assert black.Cache.read(mode).file_data == {}
 
     def test_write_cache_read_cache(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir() as workspace:
             src = (workspace / "test.py").resolve()
             src.touch()
-            black.write_cache({}, [src], mode)
-            cache = black.read_cache(mode)
-            assert str(src) in cache
-            assert cache[str(src)] == black.get_cache_info(src)
+            write_cache = black.Cache.read(mode)
+            write_cache.write([src])
+            read_cache = black.Cache.read(mode)
+            assert not read_cache.is_changed(src)
 
+    @pytest.mark.incompatible_with_mypyc
     def test_filter_cached(self) -> None:
         with TemporaryDirectory() as workspace:
             path = Path(workspace)
@@ -1658,45 +2082,91 @@ class TestCaching:
             uncached.touch()
             cached.touch()
             cached_but_changed.touch()
-            cache = {
-                str(cached): black.get_cache_info(cached),
-                str(cached_but_changed): (0.0, 0),
-            }
-            todo, done = black.filter_cached(
-                cache, {uncached, cached, cached_but_changed}
-            )
+            cache = black.Cache.read(DEFAULT_MODE)
+
+            orig_func = black.Cache.get_file_data
+
+            def wrapped_func(path: Path) -> FileData:
+                if path == cached:
+                    return orig_func(path)
+                if path == cached_but_changed:
+                    return FileData(0.0, 0, "")
+                raise AssertionError
+
+            with patch.object(black.Cache, "get_file_data", side_effect=wrapped_func):
+                cache.write([cached, cached_but_changed])
+            todo, done = cache.filtered_cached({uncached, cached, cached_but_changed})
             assert todo == {uncached, cached_but_changed}
             assert done == {cached}
 
+    def test_filter_cached_hash(self) -> None:
+        with TemporaryDirectory() as workspace:
+            path = Path(workspace)
+            src = (path / "test.py").resolve()
+            src.write_text("print('hello')", encoding="utf-8")
+            st = src.stat()
+            cache = black.Cache.read(DEFAULT_MODE)
+            cache.write([src])
+            cached_file_data = cache.file_data[str(src)]
+
+            todo, done = cache.filtered_cached([src])
+            assert todo == set()
+            assert done == {src}
+            assert cached_file_data.st_mtime == st.st_mtime
+
+            # Modify st_mtime
+            cached_file_data = cache.file_data[str(src)] = FileData(
+                cached_file_data.st_mtime - 1,
+                cached_file_data.st_size,
+                cached_file_data.hash,
+            )
+            todo, done = cache.filtered_cached([src])
+            assert todo == set()
+            assert done == {src}
+            assert cached_file_data.st_mtime < st.st_mtime
+            assert cached_file_data.st_size == st.st_size
+            assert cached_file_data.hash == black.Cache.hash_digest(src)
+
+            # Modify contents
+            src.write_text("print('hello world')", encoding="utf-8")
+            new_st = src.stat()
+            todo, done = cache.filtered_cached([src])
+            assert todo == {src}
+            assert done == set()
+            assert cached_file_data.st_mtime < new_st.st_mtime
+            assert cached_file_data.st_size != new_st.st_size
+            assert cached_file_data.hash != black.Cache.hash_digest(src)
+
     def test_write_cache_creates_directory_if_needed(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir(exists=False) as workspace:
             assert not workspace.exists()
-            black.write_cache({}, [], mode)
+            cache = black.Cache.read(mode)
+            cache.write([])
             assert workspace.exists()
 
     @event_loop()
     def test_failed_formatting_does_not_get_cached(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir() as workspace, patch(
-            "black.ProcessPoolExecutor", new=ThreadPoolExecutor
+            "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
         ):
             failing = (workspace / "failing.py").resolve()
-            with failing.open("w") as fobj:
-                fobj.write("not actually python")
+            failing.write_text("not actually python", encoding="utf-8")
             clean = (workspace / "clean.py").resolve()
-            with clean.open("w") as fobj:
-                fobj.write('print("hello")\n')
+            clean.write_text('print("hello")\n', encoding="utf-8")
             invokeBlack([str(workspace)], exit_code=123)
-            cache = black.read_cache(mode)
-            assert str(failing) not in cache
-            assert str(clean) in cache
+            cache = black.Cache.read(mode)
+            assert cache.is_changed(failing)
+            assert not cache.is_changed(clean)
 
     def test_write_cache_write_fail(self) -> None:
         mode = DEFAULT_MODE
-        with cache_dir(), patch.object(Path, "open") as mock:
-            mock.side_effect = OSError
-            black.write_cache({}, [], mode)
+        with cache_dir():
+            cache = black.Cache.read(mode)
+            with patch.object(Path, "open") as mock:
+                mock.side_effect = OSError
+                cache.write([])
 
     def test_read_cache_line_lengths(self) -> None:
         mode = DEFAULT_MODE
@@ -1704,17 +2174,19 @@ class TestCaching:
         with cache_dir() as workspace:
             path = (workspace / "file.py").resolve()
             path.touch()
-            black.write_cache({}, [path], mode)
-            one = black.read_cache(mode)
-            assert str(path) in one
-            two = black.read_cache(short_mode)
-            assert str(path) not in two
+            cache = black.Cache.read(mode)
+            cache.write([path])
+            one = black.Cache.read(mode)
+            assert not one.is_changed(path)
+            two = black.Cache.read(short_mode)
+            assert two.is_changed(path)
 
 
 def assert_collected_sources(
     src: Sequence[Union[str, Path]],
     expected: Sequence[Union[str, Path]],
     *,
+    root: Optional[Path] = None,
     exclude: Optional[str] = None,
     include: Optional[str] = None,
     extend_exclude: Optional[str] = None,
@@ -1730,7 +2202,7 @@ def assert_collected_sources(
     )
     gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
     collected = black.get_sources(
-        ctx=FakeContext(),
+        root=root or THIS_DIR,
         src=gs_src,
         quiet=False,
         verbose=False,
@@ -1741,7 +2213,7 @@ def assert_collected_sources(
         report=black.Report(),
         stdin_filename=stdin_filename,
     )
-    assert sorted(list(collected)) == sorted(gs_expected)
+    assert sorted(collected) == sorted(gs_expected)
 
 
 class TestFileCollection:
@@ -1766,9 +2238,18 @@ class TestFileCollection:
             base / "b/.definitely_exclude/a.pyi",
         ]
         src = [base / "b/"]
-        assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
+        assert_collected_sources(src, expected, root=base, extend_exclude=r"/exclude/")
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    def test_gitignore_used_on_multiple_sources(self) -> None:
+        root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
+        expected = [
+            root / "dir1" / "b.py",
+            root / "dir2" / "b.py",
+        ]
+        src = [root / "dir1", root / "dir2"]
+        assert_collected_sources(src, expected, root=root)
+
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     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.
@@ -1801,7 +2282,7 @@ class TestFileCollection:
                 None,
                 None,
                 report,
-                gitignore,
+                {path: gitignore},
                 verbose=False,
                 quiet=False,
             )
@@ -1830,13 +2311,20 @@ class TestFileCollection:
                 None,
                 None,
                 report,
-                root_gitignore,
+                {path: root_gitignore},
                 verbose=False,
                 quiet=False,
             )
         )
         assert sorted(expected) == sorted(sources)
 
+    def test_nested_gitignore_directly_in_source_directory(self) -> None:
+        # https://github.com/psf/black/issues/2598
+        path = Path(DATA_DIR / "nested_gitignore_tests")
+        src = Path(path / "root" / "child")
+        expected = [src / "a.py", src / "c.py"]
+        assert_collected_sources([src], expected)
+
     def test_invalid_gitignore(self) -> None:
         path = THIS_DIR / "data" / "invalid_gitignore_tests"
         empty_config = path / "pyproject.toml"
@@ -1861,6 +2349,26 @@ class TestFileCollection:
         gitignore = path / "a" / ".gitignore"
         assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
 
+    def test_gitignore_that_ignores_subfolders(self) -> None:
+        # If gitignore with */* is in root
+        root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
+        expected = [root / "b.py"]
+        assert_collected_sources([root], expected, root=root)
+
+        # If .gitignore with */* is nested
+        root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
+        expected = [
+            root / "a.py",
+            root / "subdir" / "b.py",
+        ]
+        assert_collected_sources([root], expected, root=root)
+
+        # If command is executed from outer dir
+        root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
+        target = root / "subdir"
+        expected = [target / "b.py"]
+        assert_collected_sources([target], expected, root=root)
+
     def test_empty_include(self) -> None:
         path = DATA_DIR / "include_exclude_tests"
         src = [path]
@@ -1891,72 +2399,57 @@ class TestFileCollection:
             src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
         )
 
-    def test_symlink_out_of_root_directory(self) -> None:
+    @pytest.mark.incompatible_with_mypyc
+    def test_symlinks(self) -> None:
         path = MagicMock()
         root = THIS_DIR.resolve()
-        child = MagicMock()
         include = re.compile(black.DEFAULT_INCLUDES)
         exclude = re.compile(black.DEFAULT_EXCLUDES)
         report = black.Report()
         gitignore = PathSpec.from_lines("gitwildmatch", [])
-        # `child` should behave like a symlink which resolved path is clearly
-        # outside of the `root` directory.
-        path.iterdir.return_value = [child]
-        child.resolve.return_value = Path("/a/b/c")
-        child.as_posix.return_value = "/a/b/c"
-        child.is_symlink.return_value = True
-        try:
-            list(
-                black.gen_python_files(
-                    path.iterdir(),
-                    root,
-                    include,
-                    exclude,
-                    None,
-                    None,
-                    report,
-                    gitignore,
-                    verbose=False,
-                    quiet=False,
-                )
+
+        regular = MagicMock()
+        outside_root_symlink = MagicMock()
+        ignored_symlink = MagicMock()
+
+        path.iterdir.return_value = [regular, outside_root_symlink, ignored_symlink]
+
+        regular.absolute.return_value = root / "regular.py"
+        regular.resolve.return_value = root / "regular.py"
+        regular.is_dir.return_value = False
+
+        outside_root_symlink.absolute.return_value = root / "symlink.py"
+        outside_root_symlink.resolve.return_value = Path("/nowhere")
+
+        ignored_symlink.absolute.return_value = root / ".mypy_cache" / "symlink.py"
+
+        files = list(
+            black.gen_python_files(
+                path.iterdir(),
+                root,
+                include,
+                exclude,
+                None,
+                None,
+                report,
+                {path: gitignore},
+                verbose=False,
+                quiet=False,
             )
-        except ValueError as ve:
-            pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
+        )
+        assert files == [regular]
+
         path.iterdir.assert_called_once()
-        child.resolve.assert_called_once()
-        child.is_symlink.assert_called_once()
-        # `child` should behave like a strange file which resolved path is clearly
-        # outside of the `root` directory.
-        child.is_symlink.return_value = False
-        with pytest.raises(ValueError):
-            list(
-                black.gen_python_files(
-                    path.iterdir(),
-                    root,
-                    include,
-                    exclude,
-                    None,
-                    None,
-                    report,
-                    gitignore,
-                    verbose=False,
-                    quiet=False,
-                )
-            )
-        path.iterdir.assert_called()
-        assert path.iterdir.call_count == 2
-        child.resolve.assert_called()
-        assert child.resolve.call_count == 2
-        child.is_symlink.assert_called()
-        assert child.is_symlink.call_count == 2
-
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+        outside_root_symlink.resolve.assert_called_once()
+        ignored_symlink.resolve.assert_not_called()
+
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin(self) -> None:
         src = ["-"]
         expected = ["-"]
         assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename(self) -> None:
         src = ["-"]
         stdin_filename = str(THIS_DIR / "data/collections.py")
@@ -1968,7 +2461,7 @@ class TestFileCollection:
             stdin_filename=stdin_filename,
         )
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
         # Exclude shouldn't exclude stdin_filename since it is mimicking the
         # file being passed directly. This is the same as
@@ -1984,7 +2477,7 @@ class TestFileCollection:
             stdin_filename=stdin_filename,
         )
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
         # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
         # file being passed directly. This is the same as
@@ -2000,7 +2493,7 @@ class TestFileCollection:
             stdin_filename=stdin_filename,
         )
 
-    @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+    @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
     def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
         # Force exclude should exclude the file when passing it through
         # stdin_filename
@@ -2014,11 +2507,52 @@ class TestFileCollection:
         )
 
 
-with open(black.__file__, "r", encoding="utf-8") as _bf:
-    black_source_lines = _bf.readlines()
+class TestDeFactoAPI:
+    """Test that certain symbols that are commonly used externally keep working.
+
+    We don't (yet) formally expose an API (see issue #779), but we should endeavor to
+    keep certain functions that external users commonly rely on working.
+
+    """
+
+    def test_format_str(self) -> None:
+        # format_str and Mode should keep working
+        assert (
+            black.format_str("print('hello')", mode=black.Mode()) == 'print("hello")\n'
+        )
+
+        # you can pass line length
+        assert (
+            black.format_str("print('hello')", mode=black.Mode(line_length=42))
+            == 'print("hello")\n'
+        )
+
+        # invalid input raises InvalidInput
+        with pytest.raises(black.InvalidInput):
+            black.format_str("syntax error", mode=black.Mode())
+
+    def test_format_file_contents(self) -> None:
+        # You probably should be using format_str() instead, but let's keep
+        # this one around since people do use it
+        assert (
+            black.format_file_contents("x=1", fast=True, mode=black.Mode()) == "x = 1\n"
+        )
+
+        with pytest.raises(black.NothingChanged):
+            black.format_file_contents("x = 1\n", fast=True, mode=black.Mode())
+
+
+try:
+    with open(black.__file__, "r", encoding="utf-8") as _bf:
+        black_source_lines = _bf.readlines()
+except UnicodeDecodeError:
+    if not black.COMPILED:
+        raise
 
 
-def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
+def tracefunc(
+    frame: types.FrameType, event: str, arg: Any
+) -> Callable[[types.FrameType, str, Any], Any]:
     """Show function calls `from black/__init__.py` as they happen.
 
     Register this with `sys.settrace()` in a test you're debugging.
index cc750b40567d7c4c0f393a272780f41115162b4e..59703036dc07dfd7a41a804a60f4762124b77ed1 100644 (file)
@@ -1,19 +1,33 @@
 import re
+from typing import TYPE_CHECKING, Any, Callable, TypeVar
 from unittest.mock import patch
 
-from click.testing import CliRunner
 import pytest
+from click.testing import CliRunner
 
-from tests.util import read_data, DETERMINISTIC_HEADER
+from tests.util import DETERMINISTIC_HEADER, read_data
 
 try:
-    import blackd
-    from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
     from aiohttp import web
-except ImportError:
-    has_blackd_deps = False
+    from aiohttp.test_utils import AioHTTPTestCase
+
+    import blackd
+except ImportError as e:
+    raise RuntimeError("Please install Black with the 'd' extra") from e
+
+if TYPE_CHECKING:
+    F = TypeVar("F", bound=Callable[..., Any])
+
+    unittest_run_loop: Callable[[F], F] = lambda x: x
 else:
-    has_blackd_deps = True
+    try:
+        from aiohttp.test_utils import unittest_run_loop
+    except ImportError:
+        # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and
+        # aiohttp 4 removed it. To maintain compatibility we can make our own
+        # no-op decorator.
+        def unittest_run_loop(func, *args, **kwargs):
+            return func
 
 
 @pytest.mark.blackd
@@ -69,7 +83,9 @@ class BlackDTestCase(AioHTTPTestCase):
     async def test_blackd_invalid_python_variant(self) -> None:
         async def check(header_value: str, expected_status: int = 400) -> None:
             response = await self.client.post(
-                "/", data=b"what", headers={blackd.PYTHON_VARIANT_HEADER: header_value}
+                "/",
+                data=b"what",
+                headers={blackd.PYTHON_VARIANT_HEADER: header_value},
             )
             self.assertEqual(response.status, expected_status)
 
@@ -77,6 +93,9 @@ class BlackDTestCase(AioHTTPTestCase):
         await check("ruby3.5")
         await check("pyi3.6")
         await check("py1.5")
+        await check("2")
+        await check("2.7")
+        await check("py2.7")
         await check("2.8")
         await check("py2.8")
         await check("3.0")
@@ -85,7 +104,7 @@ class BlackDTestCase(AioHTTPTestCase):
 
     @unittest_run_loop
     async def test_blackd_pyi(self) -> None:
-        source, expected = read_data("stub.pyi")
+        source, expected = read_data("cases", "stub.py")
         response = await self.client.post(
             "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"}
         )
@@ -95,11 +114,11 @@ class BlackDTestCase(AioHTTPTestCase):
     @unittest_run_loop
     async def test_blackd_diff(self) -> None:
         diff_header = re.compile(
-            r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+            r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
         )
 
-        source, _ = read_data("blackd_diff.py")
-        expected, _ = read_data("blackd_diff.diff")
+        source, _ = read_data("miscellaneous", "blackd_diff")
+        expected, _ = read_data("miscellaneous", "blackd_diff.diff")
 
         response = await self.client.post(
             "/", data=source, headers={blackd.DIFF_HEADER: "true"}
@@ -137,10 +156,6 @@ class BlackDTestCase(AioHTTPTestCase):
         await check("py36,py37", 200)
         await check("36", 200)
         await check("3.6.4", 200)
-
-        await check("2", 204)
-        await check("2.7", 204)
-        await check("py2.7", 204)
         await check("3.4", 204)
         await check("py3.4", 204)
         await check("py34,py36", 204)
@@ -156,10 +171,33 @@ class BlackDTestCase(AioHTTPTestCase):
     @unittest_run_loop
     async def test_blackd_invalid_line_length(self) -> None:
         response = await self.client.post(
-            "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "NaN"}
+            "/",
+            data=b'print("hello")\n',
+            headers={blackd.LINE_LENGTH_HEADER: "NaN"},
         )
         self.assertEqual(response.status, 400)
 
+    @unittest_run_loop
+    async def test_blackd_skip_first_source_line(self) -> None:
+        invalid_first_line = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
+        expected_result = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
+        response = await self.client.post("/", data=invalid_first_line)
+        self.assertEqual(response.status, 400)
+        response = await self.client.post(
+            "/",
+            data=invalid_first_line,
+            headers={blackd.SKIP_SOURCE_FIRST_LINE: "true"},
+        )
+        self.assertEqual(response.status, 200)
+        self.assertEqual(await response.read(), expected_result)
+
+    @unittest_run_loop
+    async def test_blackd_preview(self) -> None:
+        response = await self.client.post(
+            "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"}
+        )
+        self.assertEqual(response.status, 204)
+
     @unittest_run_loop
     async def test_blackd_response_black_version_header(self) -> None:
         response = await self.client.post("/")
@@ -185,3 +223,26 @@ class BlackDTestCase(AioHTTPTestCase):
         response = await self.client.post("/", headers={"Origin": "*"})
         self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin"))
         self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers"))
+
+    @unittest_run_loop
+    async def test_preserves_line_endings(self) -> None:
+        for data in (b"c\r\nc\r\n", b"l\nl\n"):
+            # test preserved newlines when reformatted
+            response = await self.client.post("/", data=data + b" ")
+            self.assertEqual(await response.text(), data.decode())
+            # test 204 when no change
+            response = await self.client.post("/", data=data)
+            self.assertEqual(response.status, 204)
+
+    @unittest_run_loop
+    async def test_normalizes_line_endings(self) -> None:
+        for data, expected in ((b"c\r\nc\n", "c\r\nc\r\n"), (b"l\nl\r\n", "l\nl\n")):
+            response = await self.client.post("/", data=data)
+            self.assertEqual(await response.text(), expected)
+            self.assertEqual(response.status, 200)
+
+    @unittest_run_loop
+    async def test_single_character(self) -> None:
+        response = await self.client.post("/", data="1")
+        self.assertEqual(await response.text(), "1\n")
+        self.assertEqual(response.status, 200)
index a659382092ac7251ce6d9be97d09b856adc0a810..4e863c6c54b4ddb843434c4d4586531a877645d5 100644 (file)
@@ -5,114 +5,15 @@ from unittest.mock import patch
 import pytest
 
 import black
+from black.mode import TargetVersion
 from tests.util import (
-    DEFAULT_MODE,
-    PY36_VERSIONS,
-    THIS_DIR,
+    all_data_cases,
     assert_format,
     dump_to_stderr,
     read_data,
+    read_data_with_mode,
 )
 
-SIMPLE_CASES = [
-    "beginning_backslash",
-    "bracketmatch",
-    "class_blank_parentheses",
-    "class_methods_new_line",
-    "collections",
-    "comments",
-    "comments2",
-    "comments3",
-    "comments4",
-    "comments5",
-    "comments6",
-    "comments_non_breaking_space",
-    "comment_after_escaped_newline",
-    "composition",
-    "composition_no_trailing_comma",
-    "docstring",
-    "empty_lines",
-    "expression",
-    "fmtonoff",
-    "fmtonoff2",
-    "fmtonoff3",
-    "fmtonoff4",
-    "fmtskip",
-    "fmtskip2",
-    "fmtskip3",
-    "fmtskip4",
-    "fmtskip5",
-    "fmtskip6",
-    "fstring",
-    "function",
-    "function2",
-    "function_trailing_comma",
-    "import_spacing",
-    "remove_parens",
-    "slices",
-    "string_prefixes",
-    "tricky_unicode_symbols",
-    "tupleassign",
-]
-
-SIMPLE_CASES_PY2 = [
-    "numeric_literals_py2",
-    "python2",
-    "python2_unicode_literals",
-]
-
-EXPERIMENTAL_STRING_PROCESSING_CASES = [
-    "cantfit",
-    "comments7",
-    "long_strings",
-    "long_strings__edge_case",
-    "long_strings__regression",
-    "percent_precedence",
-]
-
-
-SOURCES = [
-    "src/black/__init__.py",
-    "src/black/__main__.py",
-    "src/black/brackets.py",
-    "src/black/cache.py",
-    "src/black/comments.py",
-    "src/black/concurrency.py",
-    "src/black/const.py",
-    "src/black/debug.py",
-    "src/black/files.py",
-    "src/black/linegen.py",
-    "src/black/lines.py",
-    "src/black/mode.py",
-    "src/black/nodes.py",
-    "src/black/numerics.py",
-    "src/black/output.py",
-    "src/black/parsing.py",
-    "src/black/report.py",
-    "src/black/rusty.py",
-    "src/black/strings.py",
-    "src/black/trans.py",
-    "src/blackd/__init__.py",
-    "src/blib2to3/pygram.py",
-    "src/blib2to3/pytree.py",
-    "src/blib2to3/pgen2/conv.py",
-    "src/blib2to3/pgen2/driver.py",
-    "src/blib2to3/pgen2/grammar.py",
-    "src/blib2to3/pgen2/literals.py",
-    "src/blib2to3/pgen2/parse.py",
-    "src/blib2to3/pgen2/pgen.py",
-    "src/blib2to3/pgen2/tokenize.py",
-    "src/blib2to3/pgen2/token.py",
-    "setup.py",
-    "tests/test_black.py",
-    "tests/test_blackd.py",
-    "tests/test_format.py",
-    "tests/test_primer.py",
-    "tests/optional.py",
-    "tests/util.py",
-    "tests/conftest.py",
-]
-
 
 @pytest.fixture(autouse=True)
 def patch_dump_to_file(request: Any) -> Iterator[None]:
@@ -120,36 +21,33 @@ def patch_dump_to_file(request: Any) -> Iterator[None]:
         yield
 
 
-def check_file(filename: str, mode: black.Mode, *, data: bool = True) -> None:
-    source, expected = read_data(filename, data=data)
-    assert_format(source, expected, mode, fast=False)
-
-
-@pytest.mark.parametrize("filename", SIMPLE_CASES_PY2)
-@pytest.mark.python2
-def test_simple_format_py2(filename: str) -> None:
-    check_file(filename, DEFAULT_MODE)
-
-
-@pytest.mark.parametrize("filename", SIMPLE_CASES)
+def check_file(subdir: str, filename: str, *, data: bool = True) -> None:
+    args, source, expected = read_data_with_mode(subdir, filename, data=data)
+    assert_format(
+        source,
+        expected,
+        args.mode,
+        fast=args.fast,
+        minimum_version=args.minimum_version,
+    )
+    if args.minimum_version is not None:
+        major, minor = args.minimum_version
+        target_version = TargetVersion[f"PY{major}{minor}"]
+        mode = replace(args.mode, target_versions={target_version})
+        assert_format(
+            source, expected, mode, fast=args.fast, minimum_version=args.minimum_version
+        )
+
+
+@pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning")
+@pytest.mark.parametrize("filename", all_data_cases("cases"))
 def test_simple_format(filename: str) -> None:
-    check_file(filename, DEFAULT_MODE)
-
-
-@pytest.mark.parametrize("filename", EXPERIMENTAL_STRING_PROCESSING_CASES)
-def test_experimental_format(filename: str) -> None:
-    check_file(filename, black.Mode(experimental_string_processing=True))
-
-
-@pytest.mark.parametrize("filename", SOURCES)
-def test_source_is_formatted(filename: str) -> None:
-    path = THIS_DIR.parent / filename
-    check_file(str(path), DEFAULT_MODE, data=False)
+    check_file("cases", filename)
 
 
 # =============== #
-# Complex cases
-# ============= #
+# Unusual cases
+# =============== #
 
 
 def test_empty() -> None:
@@ -157,78 +55,10 @@ def test_empty() -> None:
     assert_format(source, expected)
 
 
-def test_pep_572() -> None:
-    source, expected = read_data("pep_572")
-    assert_format(source, expected, minimum_version=(3, 8))
-
-
-def test_pep_572_remove_parens() -> None:
-    source, expected = read_data("pep_572_remove_parens")
-    assert_format(source, expected, minimum_version=(3, 8))
-
-
-def test_pep_572_do_not_remove_parens() -> None:
-    source, expected = read_data("pep_572_do_not_remove_parens")
-    # the AST safety checks will fail, but that's expected, just make sure no
-    # parentheses are touched
-    assert_format(source, expected, fast=True)
-
-
-@pytest.mark.parametrize("major, minor", [(3, 9), (3, 10)])
-def test_pep_572_newer_syntax(major: int, minor: int) -> None:
-    source, expected = read_data(f"pep_572_py{major}{minor}")
-    assert_format(source, expected, minimum_version=(major, minor))
-
-
-def test_pep_570() -> None:
-    source, expected = read_data("pep_570")
-    assert_format(source, expected, minimum_version=(3, 8))
-
-
-def test_docstring_no_string_normalization() -> None:
-    """Like test_docstring but with string normalization off."""
-    source, expected = read_data("docstring_no_string_normalization")
-    mode = replace(DEFAULT_MODE, string_normalization=False)
-    assert_format(source, expected, mode)
-
-
-def test_long_strings_flag_disabled() -> 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)
-    assert_format(source, expected, mode)
-
-
-def test_numeric_literals() -> None:
-    source, expected = read_data("numeric_literals")
-    mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
-    assert_format(source, expected, mode)
-
-
-def test_numeric_literals_ignoring_underscores() -> None:
-    source, expected = read_data("numeric_literals_skip_underscores")
-    mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
-    assert_format(source, expected, mode)
-
-
-@pytest.mark.python2
-def test_python2_print_function() -> None:
-    source, expected = read_data("python2_print_function")
-    mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY27})
-    assert_format(source, expected, mode)
-
-
-def test_stub() -> None:
-    mode = replace(DEFAULT_MODE, is_pyi=True)
-    source, expected = read_data("stub.pyi")
-    assert_format(source, expected, mode)
-
-
-def test_python38() -> None:
-    source, expected = read_data("python38")
-    assert_format(source, expected, minimum_version=(3, 8))
-
+def test_patma_invalid() -> None:
+    source, expected = read_data("miscellaneous", "pattern_matching_invalid")
+    mode = black.Mode(target_versions={black.TargetVersion.PY310})
+    with pytest.raises(black.parsing.InvalidInput) as exc_info:
+        assert_format(source, expected, mode, minimum_version=(3, 10))
 
-def test_python39() -> None:
-    source, expected = read_data("python39")
-    assert_format(source, expected, minimum_version=(3, 9))
+    exc_info.match("Cannot parse: 10:11")
index 12f176c9341f5b91a7b85d57da905401ab96abcb..59897190304e7595a82a732273058f33b1407e46 100644 (file)
@@ -1,25 +1,35 @@
+import contextlib
 import pathlib
+import re
+from contextlib import ExitStack as does_not_raise
+from dataclasses import replace
+from typing import ContextManager
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
 from click.testing import CliRunner
-from black.handle_ipynb_magics import jupyter_dependencies_are_installed
+
 from black import (
-    main,
+    Mode,
     NothingChanged,
     format_cell,
     format_file_contents,
     format_file_in_place,
+    main,
 )
-import os
-import pytest
-from black import Mode
-from _pytest.monkeypatch import MonkeyPatch
-from py.path import local
+from black.handle_ipynb_magics import jupyter_dependencies_are_installed
+from tests.util import DATA_DIR, get_case_path, read_jupyter_notebook
 
+with contextlib.suppress(ModuleNotFoundError):
+    import IPython
 pytestmark = pytest.mark.jupyter
 pytest.importorskip("IPython", reason="IPython is an optional dependency")
 pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency")
 
 JUPYTER_MODE = Mode(is_ipynb=True)
 
+EMPTY_CONFIG = DATA_DIR / "empty_pyproject.toml"
+
 runner = CliRunner()
 
 
@@ -62,9 +72,19 @@ def test_trailing_semicolon_noop() -> None:
         format_cell(src, fast=True, mode=JUPYTER_MODE)
 
 
-def test_cell_magic() -> None:
+@pytest.mark.parametrize(
+    "mode",
+    [
+        pytest.param(JUPYTER_MODE, id="default mode"),
+        pytest.param(
+            replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}),
+            id="custom cell magics mode",
+        ),
+    ],
+)
+def test_cell_magic(mode: Mode) -> None:
     src = "%%time\nfoo =bar"
-    result = format_cell(src, fast=True, mode=JUPYTER_MODE)
+    result = format_cell(src, fast=True, mode=mode)
     expected = "%%time\nfoo = bar"
     assert result == expected
 
@@ -75,6 +95,16 @@ def test_cell_magic_noop() -> None:
         format_cell(src, fast=True, mode=JUPYTER_MODE)
 
 
+@pytest.mark.parametrize(
+    "mode",
+    [
+        pytest.param(JUPYTER_MODE, id="default mode"),
+        pytest.param(
+            replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}),
+            id="custom cell magics mode",
+        ),
+    ],
+)
 @pytest.mark.parametrize(
     "src, expected",
     (
@@ -89,10 +119,14 @@ def test_cell_magic_noop() -> None:
             id="Line magic with argument",
         ),
         pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"),
+        pytest.param(
+            "env =  %env var", "env = %env var", id="Assignment to environment variable"
+        ),
+        pytest.param("env =  %env", "env = %env", id="Assignment to magic"),
     ),
 )
-def test_magic(src: str, expected: str) -> None:
-    result = format_cell(src, fast=True, mode=JUPYTER_MODE)
+def test_magic(src: str, expected: str, mode: Mode) -> None:
+    result = format_cell(src, fast=True, mode=mode)
     assert result == expected
 
 
@@ -101,6 +135,7 @@ def test_magic(src: str, expected: str) -> None:
     (
         "%%bash\n2+2",
         "%%html --isolated\n2+2",
+        "%%writefile e.txt\n  meh\n meh",
     ),
 )
 def test_non_python_magics(src: str) -> None:
@@ -108,10 +143,15 @@ def test_non_python_magics(src: str) -> None:
         format_cell(src, fast=True, mode=JUPYTER_MODE)
 
 
+@pytest.mark.skipif(
+    IPython.version_info < (8, 3),
+    reason="Change in how TransformerManager transforms this input",
+)
 def test_set_input() -> None:
     src = "a = b??"
-    with pytest.raises(NothingChanged):
-        format_cell(src, fast=True, mode=JUPYTER_MODE)
+    expected = "??b"
+    result = format_cell(src, fast=True, mode=JUPYTER_MODE)
+    assert result == expected
 
 
 def test_input_already_contains_transformed_magic() -> None:
@@ -127,12 +167,47 @@ def test_magic_noop() -> None:
 
 
 def test_cell_magic_with_magic() -> None:
-    src = "%%t -n1\nls =!ls"
+    src = "%%timeit -n1\nls =!ls"
     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
-    expected = "%%t -n1\nls = !ls"
+    expected = "%%timeit -n1\nls = !ls"
     assert result == expected
 
 
+@pytest.mark.parametrize(
+    "mode, expected_output, expectation",
+    [
+        pytest.param(
+            JUPYTER_MODE,
+            "%%custom_python_magic -n1 -n2\nx=2",
+            pytest.raises(NothingChanged),
+            id="No change when cell magic not registered",
+        ),
+        pytest.param(
+            replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust2"}),
+            "%%custom_python_magic -n1 -n2\nx=2",
+            pytest.raises(NothingChanged),
+            id="No change when other cell magics registered",
+        ),
+        pytest.param(
+            replace(JUPYTER_MODE, python_cell_magics={"custom_python_magic", "cust1"}),
+            "%%custom_python_magic -n1 -n2\nx = 2",
+            does_not_raise(),
+            id="Correctly change when cell magic registered",
+        ),
+    ],
+)
+def test_cell_magic_with_custom_python_magic(
+    mode: Mode, expected_output: str, expectation: ContextManager[object]
+) -> None:
+    with expectation:
+        result = format_cell(
+            "%%custom_python_magic -n1 -n2\nx=2",
+            fast=True,
+            mode=mode,
+        )
+        assert result == expected_output
+
+
 def test_cell_magic_nested() -> None:
     src = "%%time\n%%time\n2+2"
     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
@@ -178,11 +253,7 @@ def test_empty_cell() -> None:
 
 
 def test_entire_notebook_empty_metadata() -> None:
-    with open(
-        os.path.join("tests", "data", "notebook_empty_metadata.ipynb"), "rb"
-    ) as fd:
-        content_bytes = fd.read()
-    content = content_bytes.decode()
+    content = read_jupyter_notebook("jupyter", "notebook_empty_metadata")
     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
     expected = (
         "{\n"
@@ -217,11 +288,7 @@ def test_entire_notebook_empty_metadata() -> None:
 
 
 def test_entire_notebook_trailing_newline() -> None:
-    with open(
-        os.path.join("tests", "data", "notebook_trailing_newline.ipynb"), "rb"
-    ) as fd:
-        content_bytes = fd.read()
-    content = content_bytes.decode()
+    content = read_jupyter_notebook("jupyter", "notebook_trailing_newline")
     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
     expected = (
         "{\n"
@@ -268,11 +335,7 @@ def test_entire_notebook_trailing_newline() -> None:
 
 
 def test_entire_notebook_no_trailing_newline() -> None:
-    with open(
-        os.path.join("tests", "data", "notebook_no_trailing_newline.ipynb"), "rb"
-    ) as fd:
-        content_bytes = fd.read()
-    content = content_bytes.decode()
+    content = read_jupyter_notebook("jupyter", "notebook_no_trailing_newline")
     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
     expected = (
         "{\n"
@@ -319,19 +382,14 @@ def test_entire_notebook_no_trailing_newline() -> None:
 
 
 def test_entire_notebook_without_changes() -> None:
-    with open(
-        os.path.join("tests", "data", "notebook_without_changes.ipynb"), "rb"
-    ) as fd:
-        content_bytes = fd.read()
-    content = content_bytes.decode()
+    content = read_jupyter_notebook("jupyter", "notebook_without_changes")
     with pytest.raises(NothingChanged):
         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
 
 
 def test_non_python_notebook() -> None:
-    with open(os.path.join("tests", "data", "non_python_notebook.ipynb"), "rb") as fd:
-        content_bytes = fd.read()
-    content = content_bytes.decode()
+    content = read_jupyter_notebook("jupyter", "non_python_notebook")
+
     with pytest.raises(NothingChanged):
         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
 
@@ -342,27 +400,22 @@ def test_empty_string() -> None:
 
 
 def test_unparseable_notebook() -> None:
-    msg = (
-        r"File 'tests[/\\]data[/\\]notebook_which_cant_be_parsed\.ipynb' "
-        r"cannot be parsed as valid Jupyter notebook\."
-    )
+    path = get_case_path("jupyter", "notebook_which_cant_be_parsed.ipynb")
+    msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
     with pytest.raises(ValueError, match=msg):
-        format_file_in_place(
-            pathlib.Path("tests") / "data/notebook_which_cant_be_parsed.ipynb",
-            fast=True,
-            mode=JUPYTER_MODE,
-        )
+        format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
 
 
 def test_ipynb_diff_with_change() -> None:
     result = runner.invoke(
         main,
         [
-            os.path.join("tests", "data", "notebook_trailing_newline.ipynb"),
+            str(get_case_path("jupyter", "notebook_trailing_newline.ipynb")),
             "--diff",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
-    expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
+    expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
     assert expected in result.output
 
 
@@ -370,8 +423,9 @@ def test_ipynb_diff_with_no_change() -> None:
     result = runner.invoke(
         main,
         [
-            os.path.join("tests", "data", "notebook_without_changes.ipynb"),
+            str(get_case_path("jupyter", "notebook_without_changes.ipynb")),
             "--diff",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
     expected = "1 file would be left unchanged."
@@ -379,75 +433,74 @@ def test_ipynb_diff_with_no_change() -> None:
 
 
 def test_cache_isnt_written_if_no_jupyter_deps_single(
-    monkeypatch: MonkeyPatch, tmpdir: local
+    monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
 ) -> None:
     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
     jupyter_dependencies_are_installed.cache_clear()
-    nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb")
-    tmp_nb = tmpdir / "notebook.ipynb"
-    with open(nb) as src, open(tmp_nb, "w") as dst:
-        dst.write(src.read())
-    monkeypatch.setattr(
-        "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
+    nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
+    tmp_nb = tmp_path / "notebook.ipynb"
+    tmp_nb.write_bytes(nb.read_bytes())
+    monkeypatch.setattr("black.jupyter_dependencies_are_installed", lambda warn: False)
+    result = runner.invoke(
+        main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
     )
-    result = runner.invoke(main, [str(tmpdir / "notebook.ipynb")])
     assert "No Python files are present to be formatted. Nothing to do" in result.output
     jupyter_dependencies_are_installed.cache_clear()
-    monkeypatch.setattr(
-        "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
+    monkeypatch.setattr("black.jupyter_dependencies_are_installed", lambda warn: True)
+    result = runner.invoke(
+        main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
     )
-    result = runner.invoke(main, [str(tmpdir / "notebook.ipynb")])
     assert "reformatted" in result.output
 
 
 def test_cache_isnt_written_if_no_jupyter_deps_dir(
-    monkeypatch: MonkeyPatch, tmpdir: local
+    monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
 ) -> None:
     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
     jupyter_dependencies_are_installed.cache_clear()
-    nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb")
-    tmp_nb = tmpdir / "notebook.ipynb"
-    with open(nb) as src, open(tmp_nb, "w") as dst:
-        dst.write(src.read())
+    nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
+    tmp_nb = tmp_path / "notebook.ipynb"
+    tmp_nb.write_bytes(nb.read_bytes())
     monkeypatch.setattr(
-        "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
+        "black.files.jupyter_dependencies_are_installed", lambda warn: False
     )
-    result = runner.invoke(main, [str(tmpdir)])
+    result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
     assert "No Python files are present to be formatted. Nothing to do" in result.output
     jupyter_dependencies_are_installed.cache_clear()
     monkeypatch.setattr(
-        "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
+        "black.files.jupyter_dependencies_are_installed", lambda warn: True
     )
-    result = runner.invoke(main, [str(tmpdir)])
+    result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
     assert "reformatted" in result.output
 
 
-def test_ipynb_flag(tmpdir: local) -> None:
-    nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb")
-    tmp_nb = tmpdir / "notebook.a_file_extension_which_is_definitely_not_ipynb"
-    with open(nb) as src, open(tmp_nb, "w") as dst:
-        dst.write(src.read())
+def test_ipynb_flag(tmp_path: pathlib.Path) -> None:
+    nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
+    tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb"
+    tmp_nb.write_bytes(nb.read_bytes())
     result = runner.invoke(
         main,
         [
             str(tmp_nb),
             "--diff",
             "--ipynb",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
-    expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
+    expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
     assert expected in result.output
 
 
 def test_ipynb_and_pyi_flags() -> None:
-    nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb")
+    nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
     result = runner.invoke(
         main,
         [
-            nb,
+            str(nb),
             "--pyi",
             "--ipynb",
             "--diff",
+            f"--config={EMPTY_CONFIG}",
         ],
     )
     assert isinstance(result.exception, SystemExit)
index bcda2d5369fc16a5c7903707cd1c5c7aa43e129f..12c820def390a61ad21df6f88de70af3daa1d7bf 100644 (file)
@@ -1,10 +1,10 @@
-import pytest
-import os
+import pathlib
 
-from tests.util import THIS_DIR
-from black import main, jupyter_dependencies_are_installed
+import pytest
 from click.testing import CliRunner
-from _pytest.tmpdir import tmpdir
+
+from black import jupyter_dependencies_are_installed, main
+from tests.util import get_case_path
 
 pytestmark = pytest.mark.no_jupyter
 
@@ -13,25 +13,24 @@ runner = CliRunner()
 
 def test_ipynb_diff_with_no_change_single() -> None:
     jupyter_dependencies_are_installed.cache_clear()
-    path = THIS_DIR / "data/notebook_trailing_newline.ipynb"
+    path = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
     result = runner.invoke(main, [str(path)])
     expected_output = (
         "Skipping .ipynb files as Jupyter dependencies are not installed.\n"
-        "You can fix this by running ``pip install black[jupyter]``\n"
+        'You can fix this by running ``pip install "black[jupyter]"``\n'
     )
     assert expected_output in result.output
 
 
-def test_ipynb_diff_with_no_change_dir(tmpdir: tmpdir) -> None:
+def test_ipynb_diff_with_no_change_dir(tmp_path: pathlib.Path) -> None:
     jupyter_dependencies_are_installed.cache_clear()
     runner = CliRunner()
-    nb = os.path.join("tests", "data", "notebook_trailing_newline.ipynb")
-    tmp_nb = tmpdir / "notebook.ipynb"
-    with open(nb) as src, open(tmp_nb, "w") as dst:
-        dst.write(src.read())
-    result = runner.invoke(main, [str(tmpdir)])
+    nb = get_case_path("jupyter", "notebook_trailing_newline.ipynb")
+    tmp_nb = tmp_path / "notebook.ipynb"
+    tmp_nb.write_bytes(nb.read_bytes())
+    result = runner.invoke(main, [str(tmp_path)])
     expected_output = (
         "Skipping .ipynb files as Jupyter dependencies are not installed.\n"
-        "You can fix this by running ``pip install black[jupyter]``\n"
+        'You can fix this by running ``pip install "black[jupyter]"``\n'
     )
     assert expected_output in result.output
diff --git a/.vim/bundle/black/tests/test_primer.py b/.vim/bundle/black/tests/test_primer.py
deleted file mode 100644 (file)
index e7f99fd..0000000
+++ /dev/null
@@ -1,233 +0,0 @@
-#!/usr/bin/env python3
-
-import asyncio
-import sys
-import unittest
-from contextlib import contextmanager
-from copy import deepcopy
-from io import StringIO
-from os import getpid
-from pathlib import Path
-from platform import system
-from subprocess import CalledProcessError
-from tempfile import TemporaryDirectory, gettempdir
-from typing import Any, Callable, Generator, Iterator, Tuple
-from unittest.mock import Mock, patch
-
-from click.testing import CliRunner
-
-from black_primer import cli, lib
-
-
-EXPECTED_ANALYSIS_OUTPUT = """\
--- primer results 📊 --
-
-68 / 69 succeeded (98.55%) ✅
-1 / 69 FAILED (1.45%) 💩
- - 0 projects disabled by config
- - 0 projects skipped due to Python version
- - 0 skipped due to long checkout
-
-Failed projects:
-
-## black:
- - Returned 69
- - stdout:
-Black didn't work
-
-"""
-FAKE_PROJECT_CONFIG = {
-    "cli_arguments": ["--unittest"],
-    "expect_formatting_changes": False,
-    "git_clone_url": "https://github.com/psf/black.git",
-}
-
-
-@contextmanager
-def capture_stdout(command: Callable, *args: Any, **kwargs: Any) -> Generator:
-    old_stdout, sys.stdout = sys.stdout, StringIO()
-    try:
-        command(*args, **kwargs)
-        sys.stdout.seek(0)
-        yield sys.stdout.read()
-    finally:
-        sys.stdout = old_stdout
-
-
-@contextmanager
-def event_loop() -> Iterator[None]:
-    policy = asyncio.get_event_loop_policy()
-    loop = policy.new_event_loop()
-    asyncio.set_event_loop(loop)
-    if sys.platform == "win32":
-        asyncio.set_event_loop(asyncio.ProactorEventLoop())
-    try:
-        yield
-    finally:
-        loop.close()
-
-
-async def raise_subprocess_error_1(*args: Any, **kwargs: Any) -> None:
-    raise CalledProcessError(1, ["unittest", "error"], b"", b"")
-
-
-async def raise_subprocess_error_123(*args: Any, **kwargs: Any) -> None:
-    raise CalledProcessError(123, ["unittest", "error"], b"", b"")
-
-
-async def return_false(*args: Any, **kwargs: Any) -> bool:
-    return False
-
-
-async def return_subproccess_output(*args: Any, **kwargs: Any) -> Tuple[bytes, bytes]:
-    return (b"stdout", b"stderr")
-
-
-async def return_zero(*args: Any, **kwargs: Any) -> int:
-    return 0
-
-
-class PrimerLibTests(unittest.TestCase):
-    def test_analyze_results(self) -> None:
-        fake_results = lib.Results(
-            {
-                "disabled": 0,
-                "failed": 1,
-                "skipped_long_checkout": 0,
-                "success": 68,
-                "wrong_py_ver": 0,
-            },
-            {"black": CalledProcessError(69, ["black"], b"Black didn't work", b"")},
-        )
-        with capture_stdout(lib.analyze_results, 69, fake_results) as analyze_stdout:
-            self.assertEqual(EXPECTED_ANALYSIS_OUTPUT, analyze_stdout)
-
-    @event_loop()
-    def test_black_run(self) -> None:
-        """Pretend to run Black to ensure we cater for all scenarios"""
-        loop = asyncio.get_event_loop()
-        project_name = "unittest"
-        repo_path = Path(gettempdir())
-        project_config = deepcopy(FAKE_PROJECT_CONFIG)
-        results = lib.Results({"failed": 0, "success": 0}, {})
-
-        # Test a successful Black run
-        with patch("black_primer.lib._gen_check_output", return_subproccess_output):
-            loop.run_until_complete(
-                lib.black_run(project_name, repo_path, project_config, results)
-            )
-        self.assertEqual(1, results.stats["success"])
-        self.assertFalse(results.failed_projects)
-
-        # Test a fail based on expecting formatting changes but not getting any
-        project_config["expect_formatting_changes"] = True
-        results = lib.Results({"failed": 0, "success": 0}, {})
-        with patch("black_primer.lib._gen_check_output", return_subproccess_output):
-            loop.run_until_complete(
-                lib.black_run(project_name, repo_path, project_config, results)
-            )
-        self.assertEqual(1, results.stats["failed"])
-        self.assertTrue(results.failed_projects)
-
-        # Test a fail based on returning 1 and not expecting formatting changes
-        project_config["expect_formatting_changes"] = False
-        results = lib.Results({"failed": 0, "success": 0}, {})
-        with patch("black_primer.lib._gen_check_output", raise_subprocess_error_1):
-            loop.run_until_complete(
-                lib.black_run(project_name, repo_path, project_config, results)
-            )
-        self.assertEqual(1, results.stats["failed"])
-        self.assertTrue(results.failed_projects)
-
-        # Test a formatting error based on returning 123
-        with patch("black_primer.lib._gen_check_output", raise_subprocess_error_123):
-            loop.run_until_complete(
-                lib.black_run(project_name, repo_path, project_config, results)
-            )
-        self.assertEqual(2, results.stats["failed"])
-
-    def test_flatten_cli_args(self) -> None:
-        fake_long_args = ["--arg", ["really/", "|long", "|regex", "|splitup"], "--done"]
-        expected = ["--arg", "really/|long|regex|splitup", "--done"]
-        self.assertEqual(expected, lib._flatten_cli_args(fake_long_args))
-
-    @event_loop()
-    def test_gen_check_output(self) -> None:
-        loop = asyncio.get_event_loop()
-        stdout, stderr = loop.run_until_complete(
-            lib._gen_check_output([lib.BLACK_BINARY, "--help"])
-        )
-        self.assertTrue("The uncompromising code formatter" in stdout.decode("utf8"))
-        self.assertEqual(None, stderr)
-
-        # TODO: Add a test to see failure works on Windows
-        if lib.WINDOWS:
-            return
-
-        false_bin = "/usr/bin/false" if system() == "Darwin" else "/bin/false"
-        with self.assertRaises(CalledProcessError):
-            loop.run_until_complete(lib._gen_check_output([false_bin]))
-
-        with self.assertRaises(asyncio.TimeoutError):
-            loop.run_until_complete(
-                lib._gen_check_output(["/bin/sleep", "2"], timeout=0.1)
-            )
-
-    @event_loop()
-    def test_git_checkout_or_rebase(self) -> None:
-        loop = asyncio.get_event_loop()
-        project_config = deepcopy(FAKE_PROJECT_CONFIG)
-        work_path = Path(gettempdir())
-
-        expected_repo_path = work_path / "black"
-        with patch("black_primer.lib._gen_check_output", return_subproccess_output):
-            returned_repo_path = loop.run_until_complete(
-                lib.git_checkout_or_rebase(work_path, project_config)
-            )
-        self.assertEqual(expected_repo_path, returned_repo_path)
-
-    @patch("sys.stdout", new_callable=StringIO)
-    @event_loop()
-    def test_process_queue(self, mock_stdout: Mock) -> None:
-        """Test the process queue on primer itself
-        - If you have non black conforming formatting in primer itself this can fail"""
-        loop = asyncio.get_event_loop()
-        config_path = Path(lib.__file__).parent / "primer.json"
-        with patch("black_primer.lib.git_checkout_or_rebase", return_false):
-            with TemporaryDirectory() as td:
-                return_val = loop.run_until_complete(
-                    lib.process_queue(str(config_path), Path(td), 2)
-                )
-                self.assertEqual(0, return_val)
-
-
-class PrimerCLITests(unittest.TestCase):
-    @event_loop()
-    def test_async_main(self) -> None:
-        loop = asyncio.get_event_loop()
-        work_dir = Path(gettempdir()) / f"primer_ut_{getpid()}"
-        args = {
-            "config": "/config",
-            "debug": False,
-            "keep": False,
-            "long_checkouts": False,
-            "rebase": False,
-            "workdir": str(work_dir),
-            "workers": 69,
-            "no_diff": False,
-        }
-        with patch("black_primer.cli.lib.process_queue", return_zero):
-            return_val = loop.run_until_complete(cli.async_main(**args))  # type: ignore
-            self.assertEqual(0, return_val)
-
-    def test_handle_debug(self) -> None:
-        self.assertTrue(cli._handle_debug(None, None, True))
-
-    def test_help_output(self) -> None:
-        runner = CliRunner()
-        result = runner.invoke(cli.main, ["--help"])
-        self.assertEqual(result.exit_code, 0)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/.vim/bundle/black/tests/test_trans.py b/.vim/bundle/black/tests/test_trans.py
new file mode 100644 (file)
index 0000000..784e852
--- /dev/null
@@ -0,0 +1,51 @@
+from typing import List, Tuple
+
+from black.trans import iter_fexpr_spans
+
+
+def test_fexpr_spans() -> None:
+    def check(
+        string: str, expected_spans: List[Tuple[int, int]], expected_slices: List[str]
+    ) -> None:
+        spans = list(iter_fexpr_spans(string))
+
+        # Checking slices isn't strictly necessary, but it's easier to verify at
+        # a glance than only spans
+        assert len(spans) == len(expected_slices)
+        for (i, j), slice in zip(spans, expected_slices):
+            assert 0 <= i <= j <= len(string)
+            assert string[i:j] == slice
+
+        assert spans == expected_spans
+
+    # Most of these test cases omit the leading 'f' and leading / closing quotes
+    # for convenience
+    # Some additional property-based tests can be found in
+    # https://github.com/psf/black/pull/2654#issuecomment-981411748
+    check("""{var}""", [(0, 5)], ["{var}"])
+    check("""f'{var}'""", [(2, 7)], ["{var}"])
+    check("""f'{1 + f() + 2 + "asdf"}'""", [(2, 24)], ["""{1 + f() + 2 + "asdf"}"""])
+    check("""text {var} text""", [(5, 10)], ["{var}"])
+    check("""text {{ {var} }} text""", [(8, 13)], ["{var}"])
+    check("""{a} {b} {c}""", [(0, 3), (4, 7), (8, 11)], ["{a}", "{b}", "{c}"])
+    check("""f'{a} {b} {c}'""", [(2, 5), (6, 9), (10, 13)], ["{a}", "{b}", "{c}"])
+    check("""{ {} }""", [(0, 6)], ["{ {} }"])
+    check("""{ {{}} }""", [(0, 8)], ["{ {{}} }"])
+    check("""{ {{{}}} }""", [(0, 10)], ["{ {{{}}} }"])
+    check("""{{ {{{}}} }}""", [(5, 7)], ["{}"])
+    check("""{{ {{{var}}} }}""", [(5, 10)], ["{var}"])
+    check("""{f"{0}"}""", [(0, 8)], ["""{f"{0}"}"""])
+    check("""{"'"}""", [(0, 5)], ["""{"'"}"""])
+    check("""{"{"}""", [(0, 5)], ["""{"{"}"""])
+    check("""{"}"}""", [(0, 5)], ["""{"}"}"""])
+    check("""{"{{"}""", [(0, 6)], ["""{"{{"}"""])
+    check("""{''' '''}""", [(0, 9)], ["""{''' '''}"""])
+    check("""{'''{'''}""", [(0, 9)], ["""{'''{'''}"""])
+    check("""{''' {'{ '''}""", [(0, 13)], ["""{''' {'{ '''}"""])
+    check(
+        '''f\'\'\'-{f"""*{f"+{f'.{x}.'}+"}*"""}-'y\\'\'\'\'''',
+        [(5, 33)],
+        ['''{f"""*{f"+{f'.{x}.'}+"}*"""}'''],
+    )
+    check(r"""{}{""", [(0, 2)], ["{}"])
+    check("""f"{'{'''''''''}\"""", [(2, 15)], ["{'{'''''''''}"])
index 84e98bb0fbde883da8d61545c935c7904641c232..a31ae0992c2886936fdc0bdb52c9c9213203df7e 100644 (file)
@@ -1,15 +1,25 @@
+import argparse
+import functools
 import os
+import shlex
 import sys
 import unittest
 from contextlib import contextmanager
+from dataclasses import dataclass, field, replace
 from functools import partial
 from pathlib import Path
 from typing import Any, Iterator, List, Optional, Tuple
 
 import black
+from black.const import DEFAULT_LINE_LENGTH
 from black.debug import DebugVisitor
 from black.mode import TargetVersion
-from black.output import err, out
+from black.output import diff, err, out
+
+from . import conftest
+
+PYTHON_SUFFIX = ".py"
+ALLOWED_SUFFIXES = (PYTHON_SUFFIX, ".pyi", ".out", ".diff", ".ipynb")
 
 THIS_DIR = Path(__file__).parent
 DATA_DIR = THIS_DIR / "data"
@@ -29,27 +39,53 @@ ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True)
 fs = partial(black.format_str, mode=DEFAULT_MODE)
 
 
+@dataclass
+class TestCaseArgs:
+    mode: black.Mode = field(default_factory=black.Mode)
+    fast: bool = False
+    minimum_version: Optional[Tuple[int, int]] = None
+
+
 def _assert_format_equal(expected: str, actual: str) -> None:
-    if actual != expected and not os.environ.get("SKIP_AST_PRINT"):
+    if actual != expected and (conftest.PRINT_FULL_TREE or conftest.PRINT_TREE_DIFF):
         bdv: DebugVisitor[Any]
-        out("Expected tree:", fg="green")
+        actual_out: str = ""
+        expected_out: str = ""
+        if conftest.PRINT_FULL_TREE:
+            out("Expected tree:", fg="green")
         try:
             exp_node = black.lib2to3_parse(expected)
-            bdv = DebugVisitor()
+            bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE)
             list(bdv.visit(exp_node))
+            expected_out = "\n".join(bdv.list_output)
         except Exception as ve:
             err(str(ve))
-        out("Actual tree:", fg="red")
+        if conftest.PRINT_FULL_TREE:
+            out("Actual tree:", fg="red")
         try:
             exp_node = black.lib2to3_parse(actual)
-            bdv = DebugVisitor()
+            bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE)
             list(bdv.visit(exp_node))
+            actual_out = "\n".join(bdv.list_output)
         except Exception as ve:
             err(str(ve))
+        if conftest.PRINT_TREE_DIFF:
+            out("Tree Diff:")
+            out(
+                diff(expected_out, actual_out, "expected tree", "actual tree")
+                or "Trees do not differ"
+            )
+
+    if actual != expected:
+        out(diff(expected, actual, "expected", "actual"))
 
     assert actual == expected
 
 
+class FormatFailure(Exception):
+    """Used to wrap failures when assert_format() runs in an extra mode."""
+
+
 def assert_format(
     source: str,
     expected: str,
@@ -64,12 +100,57 @@ def assert_format(
     safety guards so they don't just crash with a SyntaxError. Please note this is
     separate from TargetVerson Mode configuration.
     """
+    _assert_format_inner(
+        source, expected, mode, fast=fast, minimum_version=minimum_version
+    )
+
+    # For both preview and non-preview tests, ensure that Black doesn't crash on
+    # this code, but don't pass "expected" because the precise output may differ.
+    try:
+        _assert_format_inner(
+            source,
+            None,
+            replace(mode, preview=not mode.preview),
+            fast=fast,
+            minimum_version=minimum_version,
+        )
+    except Exception as e:
+        text = "non-preview" if mode.preview else "preview"
+        raise FormatFailure(
+            f"Black crashed formatting this case in {text} mode."
+        ) from e
+    # Similarly, setting line length to 1 is a good way to catch
+    # stability bugs. But only in non-preview mode because preview mode
+    # currently has a lot of line length 1 bugs.
+    try:
+        _assert_format_inner(
+            source,
+            None,
+            replace(mode, preview=False, line_length=1),
+            fast=fast,
+            minimum_version=minimum_version,
+        )
+    except Exception as e:
+        raise FormatFailure(
+            "Black crashed formatting this case with line-length set to 1."
+        ) from e
+
+
+def _assert_format_inner(
+    source: str,
+    expected: Optional[str] = None,
+    mode: black.Mode = DEFAULT_MODE,
+    *,
+    fast: bool = False,
+    minimum_version: Optional[Tuple[int, int]] = None,
+) -> None:
     actual = black.format_str(source, mode=mode)
-    _assert_format_equal(expected, actual)
+    if expected is not None:
+        _assert_format_equal(expected, actual)
     # It's not useful to run safety checks if we're expecting no changes anyway. The
     # assertion right above will raise if reality does actually make changes. This just
     # avoids wasted CPU cycles.
-    if not fast and source != expected:
+    if not fast and source != actual:
         # Unfortunately the AST equivalence check relies on the built-in ast module
         # being able to parse the code being formatted. This doesn't always work out
         # when checking modern code on older versions.
@@ -87,21 +168,106 @@ class BlackBaseTestCase(unittest.TestCase):
         _assert_format_equal(expected, actual)
 
 
-def read_data(name: str, data: bool = True) -> Tuple[str, str]:
+def get_base_dir(data: bool) -> Path:
+    return DATA_DIR if data else PROJECT_ROOT
+
+
+def all_data_cases(subdir_name: str, data: bool = True) -> List[str]:
+    cases_dir = get_base_dir(data) / subdir_name
+    assert cases_dir.is_dir()
+    return [case_path.stem for case_path in cases_dir.iterdir()]
+
+
+def get_case_path(
+    subdir_name: str, name: str, data: bool = True, suffix: str = PYTHON_SUFFIX
+) -> Path:
+    """Get case path from name"""
+    case_path = get_base_dir(data) / subdir_name / name
+    if not name.endswith(ALLOWED_SUFFIXES):
+        case_path = case_path.with_suffix(suffix)
+    assert case_path.is_file(), f"{case_path} is not a file."
+    return case_path
+
+
+def read_data_with_mode(
+    subdir_name: str, name: str, data: bool = True
+) -> Tuple[TestCaseArgs, str, str]:
+    """read_data_with_mode('test_name') -> Mode(), 'input', 'output'"""
+    return read_data_from_file(get_case_path(subdir_name, name, data))
+
+
+def read_data(subdir_name: str, name: str, data: bool = True) -> Tuple[str, str]:
     """read_data('test_name') -> 'input', 'output'"""
-    if not name.endswith((".py", ".pyi", ".out", ".diff")):
-        name += ".py"
-    base_dir = DATA_DIR if data else PROJECT_ROOT
-    return read_data_from_file(base_dir / name)
+    _, input, output = read_data_with_mode(subdir_name, name, data)
+    return input, output
+
+
+def _parse_minimum_version(version: str) -> Tuple[int, int]:
+    major, minor = version.split(".")
+    return int(major), int(minor)
 
 
-def read_data_from_file(file_name: Path) -> Tuple[str, str]:
+@functools.lru_cache()
+def get_flags_parser() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--target-version",
+        action="append",
+        type=lambda val: TargetVersion[val.upper()],
+        default=(),
+    )
+    parser.add_argument("--line-length", default=DEFAULT_LINE_LENGTH, type=int)
+    parser.add_argument(
+        "--skip-string-normalization", default=False, action="store_true"
+    )
+    parser.add_argument("--pyi", default=False, action="store_true")
+    parser.add_argument("--ipynb", default=False, action="store_true")
+    parser.add_argument(
+        "--skip-magic-trailing-comma", default=False, action="store_true"
+    )
+    parser.add_argument("--preview", default=False, action="store_true")
+    parser.add_argument("--fast", default=False, action="store_true")
+    parser.add_argument(
+        "--minimum-version",
+        type=_parse_minimum_version,
+        default=None,
+        help=(
+            "Minimum version of Python where this test case is parseable. If this is"
+            " set, the test case will be run twice: once with the specified"
+            " --target-version, and once with --target-version set to exactly the"
+            " specified version. This ensures that Black's autodetection of the target"
+            " version works correctly."
+        ),
+    )
+    return parser
+
+
+def parse_mode(flags_line: str) -> TestCaseArgs:
+    parser = get_flags_parser()
+    args = parser.parse_args(shlex.split(flags_line))
+    mode = black.Mode(
+        target_versions=set(args.target_version),
+        line_length=args.line_length,
+        string_normalization=not args.skip_string_normalization,
+        is_pyi=args.pyi,
+        is_ipynb=args.ipynb,
+        magic_trailing_comma=not args.skip_magic_trailing_comma,
+        preview=args.preview,
+    )
+    return TestCaseArgs(mode=mode, fast=args.fast, minimum_version=args.minimum_version)
+
+
+def read_data_from_file(file_name: Path) -> Tuple[TestCaseArgs, str, str]:
     with open(file_name, "r", encoding="utf8") as test:
         lines = test.readlines()
     _input: List[str] = []
     _output: List[str] = []
     result = _input
+    mode = TestCaseArgs()
     for line in lines:
+        if not _input and line.startswith("# flags: "):
+            mode = parse_mode(line[len("# flags: ") :])
+            continue
         line = line.replace(EMPTY_LINE, "")
         if line.rstrip() == "# output":
             result = _output
@@ -111,7 +277,19 @@ def read_data_from_file(file_name: Path) -> Tuple[str, str]:
     if _input and not _output:
         # If there's no output marker, treat the entire file as already pre-formatted.
         _output = _input[:]
-    return "".join(_input).strip() + "\n", "".join(_output).strip() + "\n"
+    return mode, "".join(_input).strip() + "\n", "".join(_output).strip() + "\n"
+
+
+def read_jupyter_notebook(subdir_name: str, name: str, data: bool = True) -> str:
+    return read_jupyter_notebook_from_file(
+        get_case_path(subdir_name, name, data, suffix=".ipynb")
+    )
+
+
+def read_jupyter_notebook_from_file(file_name: Path) -> str:
+    with open(file_name, mode="rb") as fd:
+        content_bytes = fd.read()
+    return content_bytes.decode()
 
 
 @contextmanager
index 57f41acb3d17a04845dc69c336339f21d3640e2d..018cef993c0af0aad7d1a43da7643f76a87116a0 100644 (file)
@@ -1,11 +1,14 @@
 [tox]
-envlist = {,ci-}py{36,37,38,39,310},fuzz
+isolated_build = true
+envlist = {,ci-}py{38,39,310,311,py3},fuzz,run_self
 
 [testenv]
-setenv = PYTHONPATH = {toxinidir}/src
+setenv =
+    PYTHONPATH = {toxinidir}/src
+    PYTHONWARNDEFAULTENCODING = 1
 skip_install = True
 # We use `recreate=True` because otherwise, on the second run of `tox -e py`,
-# the `no_python2` tests would run with the Python2 extra dependencies installed.
+# the `no_jupyter` tests would run with the jupyter extra dependencies installed.
 # See https://github.com/psf/black/issues/2367.
 recreate = True
 deps =
@@ -15,19 +18,63 @@ deps =
 commands =
     pip install -e .[d]
     coverage erase
-    pytest tests --run-optional no_python2 \
-        --run-optional no_jupyter \
+    pytest tests --run-optional no_jupyter \
         !ci: --numprocesses auto \
         --cov {posargs}
-    pip install -e .[d,python2]
-    pytest tests --run-optional python2 \
-        --run-optional no_jupyter \
+    pip install -e .[jupyter]
+    pytest tests --run-optional jupyter \
+        -jupyter \
         !ci: --numprocesses auto \
         --cov --cov-append {posargs}
+    coverage report
+
+[testenv:{,ci-}pypy3]
+setenv = PYTHONPATH = {toxinidir}/src
+skip_install = True
+recreate = True
+deps =
+    -r{toxinidir}/test_requirements.txt
+; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317
+; this seems to cause tox to wait forever
+; remove this when pypy releases the bugfix
+commands =
+    pip install -e .[d]
+    pytest tests \
+        --run-optional no_jupyter \
+        !ci: --numprocesses auto \
+        ci: --numprocesses 1
     pip install -e .[jupyter]
     pytest tests --run-optional jupyter \
         -m jupyter \
         !ci: --numprocesses auto \
+        ci: --numprocesses 1
+
+[testenv:{,ci-}311]
+setenv =
+  PYTHONPATH = {toxinidir}/src
+  AIOHTTP_NO_EXTENSIONS = 1
+skip_install = True
+recreate = True
+deps =
+; We currently need > aiohttp 3.8.1 that is on PyPI for 3.11
+    git+https://github.com/aio-libs/aiohttp
+    -r{toxinidir}/test_requirements.txt
+; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317
+; this seems to cause tox to wait forever
+; remove this when pypy releases the bugfix
+commands =
+    pip install -e .[d]
+    coverage erase
+    pytest tests \
+        --run-optional no_jupyter \
+        !ci: --numprocesses auto \
+        ci: --numprocesses 1 \
+        --cov {posargs}
+    pip install -e .[jupyter]
+    pytest tests --run-optional jupyter \
+        -m jupyter \
+        !ci: --numprocesses auto \
+        ci: --numprocesses 1 \
         --cov --cov-append {posargs}
     coverage report
 
@@ -36,10 +83,16 @@ skip_install = True
 deps =
     -r{toxinidir}/test_requirements.txt
     hypothesmith
-    lark-parser < 0.10.0
-; lark-parser's version is set due to a bug in hypothesis. Once it solved, that would be fixed.
+    lark-parser
 commands =
     pip install -e .[d]
     coverage erase
-    coverage run fuzz.py
+    coverage run {toxinidir}/scripts/fuzz.py
     coverage report
+
+[testenv:run_self]
+setenv = PYTHONPATH = {toxinidir}/src
+skip_install = True
+commands =
+    pip install -e .[d]
+    black --check {toxinidir}/src {toxinidir}/tests