]> 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' master
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)
287 files changed:
1  2 
.vim/bundle/black/.flake8
.vim/bundle/black/.git_archival.txt
.vim/bundle/black/.gitattributes
.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
.vim/bundle/black/.github/workflows/changelog.yml
.vim/bundle/black/.github/workflows/diff_shades.yml
.vim/bundle/black/.github/workflows/diff_shades_comment.yml
.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/pypi_upload.yml
.vim/bundle/black/.github/workflows/test.yml
.vim/bundle/black/.github/workflows/upload_binary.yml
.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
.vim/bundle/black/Dockerfile
.vim/bundle/black/README.md
.vim/bundle/black/SECURITY.md
.vim/bundle/black/action.yml
.vim/bundle/black/action/main.py
.vim/bundle/black/autoload/black.vim
.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
.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
.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
.vim/bundle/black/docs/integrations/editors.md
.vim/bundle/black/docs/integrations/github_actions.md
.vim/bundle/black/docs/integrations/index.md
.vim/bundle/black/docs/integrations/source_version_control.md
.vim/bundle/black/docs/license.md
.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
.vim/bundle/black/docs/usage_and_configuration/black_as_a_server.md
.vim/bundle/black/docs/usage_and_configuration/black_docker_image.md
.vim/bundle/black/docs/usage_and_configuration/file_collection_and_discovery.md
.vim/bundle/black/docs/usage_and_configuration/index.md
.vim/bundle/black/docs/usage_and_configuration/the_basics.md
.vim/bundle/black/gallery/gallery.py
.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
.vim/bundle/black/scripts/fuzz.py
.vim/bundle/black/scripts/make_width_table.py
.vim/bundle/black/scripts/migrate-black.py
.vim/bundle/black/src/black/__init__.py
.vim/bundle/black/src/black/_width_table.py
.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/blackd/__init__.py
.vim/bundle/black/src/blackd/__main__.py
.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
.vim/bundle/black/tests/data/cases/beginning_backslash.py
.vim/bundle/black/tests/data/cases/bracketmatch.py
.vim/bundle/black/tests/data/cases/class_blank_parentheses.py
.vim/bundle/black/tests/data/cases/class_methods_new_line.py
.vim/bundle/black/tests/data/cases/collections.py
.vim/bundle/black/tests/data/cases/comment_after_escaped_newline.py
.vim/bundle/black/tests/data/cases/comments.py
.vim/bundle/black/tests/data/cases/comments2.py
.vim/bundle/black/tests/data/cases/comments3.py
.vim/bundle/black/tests/data/cases/comments4.py
.vim/bundle/black/tests/data/cases/comments5.py
.vim/bundle/black/tests/data/cases/comments6.py
.vim/bundle/black/tests/data/cases/comments8.py
.vim/bundle/black/tests/data/cases/comments9.py
.vim/bundle/black/tests/data/cases/comments_non_breaking_space.py
.vim/bundle/black/tests/data/cases/composition.py
.vim/bundle/black/tests/data/cases/composition_no_trailing_comma.py
.vim/bundle/black/tests/data/cases/conditional_expression.py
.vim/bundle/black/tests/data/cases/docstring.py
.vim/bundle/black/tests/data/cases/docstring_no_extra_empty_line_before_eof.py
.vim/bundle/black/tests/data/cases/docstring_no_string_normalization.py
.vim/bundle/black/tests/data/cases/docstring_preview.py
.vim/bundle/black/tests/data/cases/docstring_preview_no_string_normalization.py
.vim/bundle/black/tests/data/cases/empty_lines.py
.vim/bundle/black/tests/data/cases/expression.diff
.vim/bundle/black/tests/data/cases/expression.py
.vim/bundle/black/tests/data/cases/fmtonoff.py
.vim/bundle/black/tests/data/cases/fmtonoff2.py
.vim/bundle/black/tests/data/cases/fmtonoff3.py
.vim/bundle/black/tests/data/cases/fmtonoff4.py
.vim/bundle/black/tests/data/cases/fmtonoff5.py
.vim/bundle/black/tests/data/cases/fmtpass_imports.py
.vim/bundle/black/tests/data/cases/fmtskip.py
.vim/bundle/black/tests/data/cases/fmtskip2.py
.vim/bundle/black/tests/data/cases/fmtskip3.py
.vim/bundle/black/tests/data/cases/fmtskip4.py
.vim/bundle/black/tests/data/cases/fmtskip5.py
.vim/bundle/black/tests/data/cases/fmtskip6.py
.vim/bundle/black/tests/data/cases/fmtskip7.py
.vim/bundle/black/tests/data/cases/fmtskip8.py
.vim/bundle/black/tests/data/cases/fstring.py
.vim/bundle/black/tests/data/cases/funcdef_return_type_trailing_comma.py
.vim/bundle/black/tests/data/cases/function.py
.vim/bundle/black/tests/data/cases/function2.py
.vim/bundle/black/tests/data/cases/function_trailing_comma.py
.vim/bundle/black/tests/data/cases/ignore_pyi.py
.vim/bundle/black/tests/data/cases/import_spacing.py
.vim/bundle/black/tests/data/cases/linelength6.py
.vim/bundle/black/tests/data/cases/long_strings_flag_disabled.py
.vim/bundle/black/tests/data/cases/module_docstring_1.py
.vim/bundle/black/tests/data/cases/module_docstring_2.py
.vim/bundle/black/tests/data/cases/module_docstring_3.py
.vim/bundle/black/tests/data/cases/module_docstring_4.py
.vim/bundle/black/tests/data/cases/multiline_consecutive_open_parentheses_ignore.py
.vim/bundle/black/tests/data/cases/nested_stub.py
.vim/bundle/black/tests/data/cases/numeric_literals.py
.vim/bundle/black/tests/data/cases/numeric_literals_skip_underscores.py
.vim/bundle/black/tests/data/cases/one_element_subscript.py
.vim/bundle/black/tests/data/cases/parenthesized_context_managers.py
.vim/bundle/black/tests/data/cases/pattern_matching_complex.py
.vim/bundle/black/tests/data/cases/pattern_matching_extras.py
.vim/bundle/black/tests/data/cases/pattern_matching_generic.py
.vim/bundle/black/tests/data/cases/pattern_matching_simple.py
.vim/bundle/black/tests/data/cases/pattern_matching_style.py
.vim/bundle/black/tests/data/cases/pep604_union_types_line_breaks.py
.vim/bundle/black/tests/data/cases/pep_570.py
.vim/bundle/black/tests/data/cases/pep_572.py
.vim/bundle/black/tests/data/cases/pep_572_do_not_remove_parens.py
.vim/bundle/black/tests/data/cases/pep_572_py310.py
.vim/bundle/black/tests/data/cases/pep_572_py39.py
.vim/bundle/black/tests/data/cases/pep_572_remove_parens.py
.vim/bundle/black/tests/data/cases/pep_604.py
.vim/bundle/black/tests/data/cases/pep_646.py
.vim/bundle/black/tests/data/cases/pep_654.py
.vim/bundle/black/tests/data/cases/pep_654_style.py
.vim/bundle/black/tests/data/cases/power_op_newline.py
.vim/bundle/black/tests/data/cases/power_op_spacing.py
.vim/bundle/black/tests/data/cases/prefer_rhs_split_reformatted.py
.vim/bundle/black/tests/data/cases/preview_async_stmts.py
.vim/bundle/black/tests/data/cases/preview_cantfit.py
.vim/bundle/black/tests/data/cases/preview_comments7.py
.vim/bundle/black/tests/data/cases/preview_context_managers_38.py
.vim/bundle/black/tests/data/cases/preview_context_managers_39.py
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_310.py
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_311.py
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_38.py
.vim/bundle/black/tests/data/cases/preview_context_managers_autodetect_39.py
.vim/bundle/black/tests/data/cases/preview_dummy_implementations.py
.vim/bundle/black/tests/data/cases/preview_format_unicode_escape_seq.py
.vim/bundle/black/tests/data/cases/preview_long_dict_values.py
.vim/bundle/black/tests/data/cases/preview_long_strings.py
.vim/bundle/black/tests/data/cases/preview_long_strings__east_asian_width.py
.vim/bundle/black/tests/data/cases/preview_long_strings__edge_case.py
.vim/bundle/black/tests/data/cases/preview_long_strings__regression.py
.vim/bundle/black/tests/data/cases/preview_long_strings__type_annotations.py
.vim/bundle/black/tests/data/cases/preview_multiline_strings.py
.vim/bundle/black/tests/data/cases/preview_no_blank_line_before_docstring.py
.vim/bundle/black/tests/data/cases/preview_pep_572.py
.vim/bundle/black/tests/data/cases/preview_percent_precedence.py
.vim/bundle/black/tests/data/cases/preview_power_op_spacing.py
.vim/bundle/black/tests/data/cases/preview_prefer_rhs_split.py
.vim/bundle/black/tests/data/cases/preview_return_annotation_brackets_string.py
.vim/bundle/black/tests/data/cases/preview_trailing_comma.py
.vim/bundle/black/tests/data/cases/py310_pep572.py
.vim/bundle/black/tests/data/cases/python37.py
.vim/bundle/black/tests/data/cases/python38.py
.vim/bundle/black/tests/data/cases/python39.py
.vim/bundle/black/tests/data/cases/remove_await_parens.py
.vim/bundle/black/tests/data/cases/remove_except_parens.py
.vim/bundle/black/tests/data/cases/remove_for_brackets.py
.vim/bundle/black/tests/data/cases/remove_newline_after_code_block_open.py
.vim/bundle/black/tests/data/cases/remove_newline_after_match.py
.vim/bundle/black/tests/data/cases/remove_parens.py
.vim/bundle/black/tests/data/cases/remove_with_brackets.py
.vim/bundle/black/tests/data/cases/return_annotation_brackets.py
.vim/bundle/black/tests/data/cases/skip_magic_trailing_comma.py
.vim/bundle/black/tests/data/cases/slices.py
.vim/bundle/black/tests/data/cases/starred_for_target.py
.vim/bundle/black/tests/data/cases/string_prefixes.py
.vim/bundle/black/tests/data/cases/stub.py
.vim/bundle/black/tests/data/cases/torture.py
.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens1.py
.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens2.py
.vim/bundle/black/tests/data/cases/trailing_comma_optional_parens3.py
.vim/bundle/black/tests/data/cases/trailing_commas_in_leading_parts.py
.vim/bundle/black/tests/data/cases/tricky_unicode_symbols.py
.vim/bundle/black/tests/data/cases/tupleassign.py
.vim/bundle/black/tests/data/cases/type_aliases.py
.vim/bundle/black/tests/data/cases/type_comment_syntax_error.py
.vim/bundle/black/tests/data/cases/type_params.py
.vim/bundle/black/tests/data/cases/whitespace.py
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/.gitignore
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir1/a.py
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir1/b.py
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/a.py
.vim/bundle/black/tests/data/gitignore_used_on_multiple_sources/dir2/b.py
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/a.py
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py
.vim/bundle/black/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py
.vim/bundle/black/tests/data/jupyter/non_python_notebook.ipynb
.vim/bundle/black/tests/data/jupyter/notebook_empty_metadata.ipynb
.vim/bundle/black/tests/data/jupyter/notebook_no_trailing_newline.ipynb
.vim/bundle/black/tests/data/jupyter/notebook_trailing_newline.ipynb
.vim/bundle/black/tests/data/jupyter/notebook_which_cant_be_parsed.ipynb
.vim/bundle/black/tests/data/jupyter/notebook_without_changes.ipynb
.vim/bundle/black/tests/data/miscellaneous/async_as_identifier.py
.vim/bundle/black/tests/data/miscellaneous/blackd_diff.diff
.vim/bundle/black/tests/data/miscellaneous/blackd_diff.py
.vim/bundle/black/tests/data/miscellaneous/debug_visitor.out
.vim/bundle/black/tests/data/miscellaneous/debug_visitor.py
.vim/bundle/black/tests/data/miscellaneous/decorators.py
.vim/bundle/black/tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff
.vim/bundle/black/tests/data/miscellaneous/force_py36.py
.vim/bundle/black/tests/data/miscellaneous/force_pyi.py
.vim/bundle/black/tests/data/miscellaneous/invalid_header.py
.vim/bundle/black/tests/data/miscellaneous/missing_final_newline.diff
.vim/bundle/black/tests/data/miscellaneous/missing_final_newline.py
.vim/bundle/black/tests/data/miscellaneous/pattern_matching_invalid.py
.vim/bundle/black/tests/data/miscellaneous/python2_detection.py
.vim/bundle/black/tests/data/miscellaneous/string_quotes.py
.vim/bundle/black/tests/data/project_metadata/both_pyproject.toml
.vim/bundle/black/tests/data/project_metadata/neither_pyproject.toml
.vim/bundle/black/tests/data/project_metadata/only_black_pyproject.toml
.vim/bundle/black/tests/data/project_metadata/only_metadata_pyproject.toml
.vim/bundle/black/tests/data/raw_docstring.py
.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_trans.py
.vim/bundle/black/tests/util.py
.vim/bundle/black/tox.ini

index ae11a13347c999a4b1f34f1d77f95f157172de9c,85f51cf9f05a4e6dd004afd7d64e9a54457f1901..85f51cf9f05a4e6dd004afd7d64e9a54457f1901
@@@ -1,5 -1,6 +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
index 0000000000000000000000000000000000000000,8fb235d7045be0330d94bcb3abd2ac43badaa197..8fb235d7045be0330d94bcb3abd2ac43badaa197
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,4 +1,4 @@@
+ node: $Format:%H$
+ node-date: $Format:%cI$
+ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
+ ref-names: $Format:%D$
index 0000000000000000000000000000000000000000,00a7b00c94e08b86c765d47689b6523148c46eec..00a7b00c94e08b86c765d47689b6523148c46eec
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1 +1,1 @@@
+ .git_archival.txt  export-subst
index e8d232c8b34c840846090534b551a073f5fb9923,48aa9291b0523359cd34eb3924bd7a724aae4e58..48aa9291b0523359cd34eb3924bd7a724aae4e58
@@@ -6,41 -6,58 +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..a9ce85fd977a1144f84da86540e7ae9708f9c22f
@@@ -1,8 -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..a039718cd7053d44610ce12c850ad520d999dc97
@@@ -18,7 -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?
  
index 0000000000000000000000000000000000000000,325cb31af1cca78965ebed4b88347b63e79dd597..325cb31af1cca78965ebed4b88347b63e79dd597
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,17 +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..a1804597d7d22fb60bde1f425a94f88c2c85e36a
@@@ -4,6 -4,9 +4,9 @@@ on
    pull_request:
      types: [opened, synchronize, labeled, unlabeled, reopened]
  
+ permissions:
+   contents: read
  jobs:
    build:
      name: Changelog Entry Check
      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)
index 0000000000000000000000000000000000000000,97db907abc8787eda5ca18f732239690185bca7a..97db907abc8787eda5ca18f732239690185bca7a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,155 +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 }}
index 0000000000000000000000000000000000000000,b86bd93410e6f32b5dc8de4ee0be7deca1489408..b86bd93410e6f32b5dc8de4ee0be7deca1489408
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,49 +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..fa3d87c70f5b6d61cd2bdae4ecb9b0fd54d36805
@@@ -1,7 -1,10 +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
  
      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..ee858236fcf1780678d3c223421711e7ca0f7a56
@@@ -7,22 -7,25 +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 +36,7 @@@
            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
            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..1b5a50c0e0b4a32faed252cedfb6fea19c9020b7
@@@ -2,6 -2,13 +2,13 @@@ name: Fuz
  
  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
      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..3eaf5785f5a675453bcd7c82a5c06b722ea6fa4b
@@@ -14,15 -14,29 +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
index 201d94fd85e931ce5e71cc9f38d1b5402e5f269b,a57013d67c1b1f4c108f034583a8d44a7b8a32f4..a57013d67c1b1f4c108f034583a8d44a7b8a32f4
- 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..1f33f2b814fd5f19700ea2c91325d46749a9ef2d
@@@ -11,8 -11,15 +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.
      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
            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..bb19d48158c25e285d3c33d64af598a8a8edae2d
@@@ -1,9 -1,12 +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 }}
              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
index f81bce8fd4e1117e092851aa0ec327decc5d5dff,4a4f1b738ad12d59e0eb37475abc05c3673b9a0e..4a4f1b738ad12d59e0eb37475abc05c3673b9a0e
@@@ -4,16 -4,22 +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..623e661ac0747bdcfaba1709524c5e6a82925a13
@@@ -1,17 -1,9 +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 +12,7 @@@
          additional_dependencies:
            &version_check_dependencies [
              commonmark==0.9.1,
-             pyyaml==5.4.1,
+             pyyaml==6.0.1,
              beautifulsoup4==4.9.3,
            ]
  
          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..a1ff41fded87d199692f1b68119c5bb390bbab5c
@@@ -1,3 -1,5 +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..fa612668850c6cbe18dc94287616ed415bb8b945
@@@ -3,8 -3,12 +3,12 @@@ version: 
  formats:
    - htmlzip
  
+ build:
+   os: ubuntu-22.04
+   tools:
+     python: "3.11"
  python:
-   version: 3.8
    install:
      - requirements: docs/requirements.txt
  
index 8d112ea679587ac68f19922686b555e4af8b47d9,e0511bb9b7ca8db6ba56205b28c2afdd07d6f370..e0511bb9b7ca8db6ba56205b28c2afdd07d6f370
@@@ -2,27 -2,36 +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 +82,7 @@@
  - [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)
  - [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..79b5c6034e8ad2044f1417a5ccd1c0f4ee1cdf25
  
  ## 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
  
index 0000000000000000000000000000000000000000,7ff0e3ca9bc075f7d30f12ebfb380c98378614b1..7ff0e3ca9bc075f7d30f12ebfb380c98378614b1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,22 +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..a9e0ea5081e1ee3f4c3b53fdd5ad85f683684b53
@@@ -1,14 -1,19 +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"]
index 7bf0ed8d16f59d767c2a2e41d45700d4f1c6d409,cad8184f7bc36ad02a010393e1bf59d430b65f1e..cad8184f7bc36ad02a010393e1bf59d430b65f1e
@@@ -1,15 -1,14 +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 +38,12 @@@ Try it out now using the [Black Playgro
  
  ### 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 +63,13 @@@ Further information can be found in ou
  
  - [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 +81,9 @@@ section for details). If you're feelin
  
  _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:
  - [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 +132,13 @@@ code in compliance with many other _Bla
  ## 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 +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!
  
index 0000000000000000000000000000000000000000,47049501183d391e3419c279929a10bfa543f4d8..47049501183d391e3419c279929a10bfa543f4d8
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,11 +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..8b698ae3c8012b278e3818a2d0e0290d69eb171b
@@@ -5,13 -5,18 +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 +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..c0af3930dbbe6f31745e941712e81df3ddb2cae3
@@@ -1,39 -1,79 +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..051fea05c3bd1d87d98cd53f59cebf78031ebcdb
@@@ -3,8 -3,13 +3,13 @@@ import collection
  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 +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:
      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 +64,19 @@@ def _get_virtualenv_site_packages(venv_
    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 +90,7 @@@
      _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 +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"]
      )
    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 = []
        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("@%")
index 7abddd2c330c4720efaa8ee910342d8d864538fa,e863488dfbc8b605882da675729d0089fb6a2594..e863488dfbc8b605882da675729d0089fb6a2594
@@@ -1,5 -1,2 +1,2 @@@
- [MESSAGES CONTROL]
- disable = C0330, C0326
  [format]
  max-line-length = 88
index 49ad7a2c7716f9329fe21509cab2a3da6a8109d3,ef51f98a966912189c428d1cfebc5067b079c768..ef51f98a966912189c428d1cfebc5067b079c768
@@@ -1,5 -1,2 +1,2 @@@
- [tool.pylint.messages_control]
- disable = "C0330, C0326"
  [tool.pylint.format]
  max-line-length = "88"
index 3ada24530ea5ac462faff3f8c21a37af3d2796b7,0b754cdc0f0a2b1a2991c14c52d9595c4b7fc6a0..0b754cdc0f0a2b1a2991c14c52d9595c4b7fc6a0
@@@ -1,5 -1,2 +1,2 @@@
  [pylint]
  max-line-length = 88
- [pylint.messages_control]
- disable = C0330, C0326
index 55d0fa99dc6c2ce290fe0c7ec9b1b31ae7590bf5,6b6454353258fa2f051f872fec8efcaef173facf..6b6454353258fa2f051f872fec8efcaef173facf
@@@ -55,7 -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 +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 +105,22 @@@ myst_heading_anchors = 
  # 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 +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..8562a83ed0c8da7d5e33d3aced1d26bdab02ca23
@@@ -7,36 -7,52 +7,52 @@@ It's recommended you evaluate the quant
  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
index 0000000000000000000000000000000000000000,3314c8eaa39f3863db95fbd056695f747c597bb7..3314c8eaa39f3863db95fbd056695f747c597bb7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,50 +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.
index 9b987fb24250cad9bdcfbb9509c198bf8c76a5cf,89cfff76f7fd1bc20e9a05c4d672cd9891e532eb..89cfff76f7fd1bc20e9a05c4d672cd9891e532eb
@@@ -1,6 -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 +42,7 @@@ The lifecycle of a bug report or user s
  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
     - 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..dc615579e307aefedadfff6260389cdd6a27c290
@@@ -3,6 -3,9 +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:
  
  .. 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..ab46ebdb628f1f7c7b69be78deb08223ffd6a5d9
@@@ -5,8 -5,14 +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..dd92e37a7d4be3792ba851fb8098f9ffa7aecf9d
@@@ -52,7 -52,7 +52,7 @@@ Formattin
  
  .. autofunction:: black.reformat_one
  
- .. autofunction:: black.schedule_formatting
+ .. autofunction:: black.concurrency.schedule_formatting
  
  File operations
  ---------------
@@@ -94,16 -94,10 +94,10 @@@ Split function
  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
  ---------
  
  
  .. 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
  
  
  .. 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..c6163d897b6bc6c7a4b182a9e6d90930ced5a6b2
@@@ -3,8 -3,11 +3,11 @@@ Developer referenc
  
  .. 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..02865d6f4bd136655f947b6061ed9b9d1e1b9812
  # 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..bc1680eecfd835e92fbffa3fd99cac39c9856ed0
@@@ -4,26 -4,20 +4,20 @@@ An overview on contributing to the _Bla
  
  ## 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 +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 +114,20 @@@ go back and workout what to add to the 
  
  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..c62e1b504b52c2486f2aa643cb88679efeac1634
@@@ -5,20 -5,32 +5,32 @@@ The most common questions and issues us
  ```{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 +38,12 @@@ other tools, such as `# noqa`, may be m
  
  ## 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 +57,8 @@@ _Black_ is timid about formatting Jupyt
  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 +84,18 @@@ readability because operators are misal
  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 +104,35 @@@ influence their behavior. While Black d
  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..15b7646a5094519ca1c8dfc8f8efc1191d123a6e
@@@ -16,14 -16,12 +16,12 @@@ Also, you can try out _Black_ online fo
  
  ## 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
  
index 0000000000000000000000000000000000000000,127279b5e81dd190d07d6f452abb202ecc603ebc..127279b5e81dd190d07d6f452abb202ecc603ebc
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,16 +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`
index 71ccf7c114cc14ceb4f96bfc8a3e7d8ea6e4c419,71a566fbda1ad60d8099d7fafaf812398ab7abcd..71a566fbda1ad60d8099d7fafaf812398ab7abcd
@@@ -43,8 -43,9 +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..22c641a74203defdc0d9aef93d36a5c3793b49f5
@@@ -51,9 -51,9 +51,9 @@@ line_length = 8
  
  #### 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 +97,7 @@@ does not break older versions so you ca
  <details>
  <summary>.isort.cfg</summary>
  
- ```cfg
+ ```ini
  [settings]
  profile = black
  ```
  <details>
  <summary>setup.cfg</summary>
  
- ```cfg
+ ```ini
  [isort]
  profile = black
  ```
@@@ -173,7 -173,7 +173,7 @@@ limit of `88`, _Black_'s default. This 
  ```ini
  [flake8]
  max-line-length = 88
- extend-ignore = E203
+ extend-ignore = E203, E704
  ```
  
  </details>
  <details>
  <summary>setup.cfg</summary>
  
- ```cfg
+ ```ini
  [flake8]
  max-line-length = 88
  extend-ignore = E203
@@@ -210,31 -210,16 +210,16 @@@ mixed feelings about _Black_'s formatti
  #### 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
  
  <summary>pylintrc</summary>
  
  ```ini
- [MESSAGES CONTROL]
- disable = C0330, C0326
  [format]
  max-line-length = 88
  ```
  ```cfg
  [pylint]
  max-line-length = 88
- [pylint.messages_control]
- disable = C0330, C0326
  ```
  
  </details>
  <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>
index 0000000000000000000000000000000000000000,49a44ecca5a38c2bd40987be5789508fe1e01ac8..49a44ecca5a38c2bd40987be5789508fe1e01ac8
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,139 +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`
index d3be7c0ea84ddd482fdb01efb7846fb370a80057,7d056160fcbccf4d9db4fb0da250e6019313617d..7d056160fcbccf4d9db4fb0da250e6019313617d
@@@ -10,6 -10,67 +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
     - 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`.
  
     $ 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 +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 +285,14 @@@ $ cd ~/.vim/bundle/blac
  $ 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 +307,7 @@@ curl https://raw.githubusercontent.com/
  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 +317,31 @@@ If you need to do anything special to m
  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 +389,20 @@@ close and reopen your File, _Black_ wil
  
  ## 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..56b2cdd05868121b1fd75a68f7040b4d35f45ba0
@@@ -8,8 -8,8 +8,8 @@@ environment. Great for enforcing that y
  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 +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 +32,21 @@@ We recommend the use of the `@stable` t
  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:
  
    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"
+ ```
index 0000000000000000000000000000000000000000,33135d08f1ad53bd87b857034e110d528b5f4d00..33135d08f1ad53bd87b857034e110d528b5f4d00
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,31 +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).
index 6a1aa363d2be3e009e5652a9acad9e5b241a3da7,16354f849ba90eacc29b2028b8e1aaa4f7f95733..16354f849ba90eacc29b2028b8e1aaa4f7f95733
@@@ -6,26 -6,48 +6,48 @@@ Use [pre-commit](https://pre-commit.com
  
  ```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
index 0000000000000000000000000000000000000000,132c95bfe2a944733147f0cd0e16fdff67c6e8fa..132c95bfe2a944733147f0cd0e16fdff67c6e8fa
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,9 +1,9 @@@
+ ---
+ orphan: true
+ ---
+ # License
+ ```{include} ../LICENSE
+ ```
index 4c5b700412a502722c06b64529556e84241238e9,b5b9e22fc84416897d176ee81314177668baf71a..b5b9e22fc84416897d176ee81314177668baf71a
@@@ -1,6 -1,9 +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..ff757a8276bd89dd96908857d9da4215b26cb756
@@@ -2,20 -2,28 +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 +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 +140,7 @@@ If you're reaching for backslashes, tha
  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 +160,35 @@@ harder to work with line lengths exceed
  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 +198,45 @@@ that in-function vertical whitespace sh
  _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 +254,12 @@@ required due to an inner function start
  
  _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 +278,18 @@@ A pre-existing trailing comma informs _
  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 +313,6 @@@ If you are adopting _Black_ in a large 
  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 +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 +333,26 @@@ multiple lines. This is so that _Black
  [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 +445,16 @@@ recommended code style for those files 
  _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 +464,8 @@@ there were not many users anyway. Not m
  _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 +502,19 @@@ default by (among others) GitHub and Vi
  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..367ff98537c8ff333bd919b62d0411e5f2d9732e
@@@ -19,24 -19,208 +19,208 @@@ with make_context_manager1() as cm1, ma
      ...  # 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
+ )
+ ```
index 0000000000000000000000000000000000000000,1719347eec81ae97a10f181e149a6fb2194acbcc..1719347eec81ae97a10f181e149a6fb2194acbcc
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,52 +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`
index 75a4d925a544397b7f445c6b6439d9ab8310890a,f24fb34d91520c4f7e48f20ce7257fe9ab022b76..f24fb34d91520c4f7e48f20ce7257fe9ab022b76
@@@ -4,10 -4,15 +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 +50,18 @@@ is rejected with `HTTP 501` (Not Implem
  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
index 0000000000000000000000000000000000000000,85aec91ef1c387fdc5d5162b037b6c97f7e73cbe..85aec91ef1c387fdc5d5162b037b6c97f7e73cbe
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,51 +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..de1d5e6c11ef61c430db24429f8b51a69e887465
@@@ -22,10 -22,12 +22,12 @@@ run. The file is non-portable. The stan
  `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
  
index 0000000000000000000000000000000000000000,1c86a49b6865610cd0b685076c66d195c107dc10..1c86a49b6865610cd0b685076c66d195c107dc10
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,28 +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>`
index 49268b44f7c03da4d175269c81f2cc69617c3469,5b132a95eae5f35be0d9573ef83f4c1956050580..5b132a95eae5f35be0d9573ef83f4c1956050580
@@@ -4,11 -4,11 +4,11 @@@ Foundational knowledge on using and con
  
  _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 +26,107 @@@ python -m black {source_file_or_directo
  
  ### 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 +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 +175,93 @@@ All done! â\9c¨ ð\9f\8d° â\9c
  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 +280,86 @@@ Oh no! ð\9f\92¥ ð\9f\92\94 ð\9f\92
  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 +376,10 @@@ code in compliance with many other _Bla
  
  [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 +432,14 @@@ expressions by Black. Use `[ ]` to deno
  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 +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..ba5d6f65fbe5c9c0034b1569d8897f28713780f0
@@@ -10,26 -10,16 +10,16 @@@ from argparse import ArgumentParser, Na
  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 +54,8 @@@ def get_pypi_download_url(package: 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 +118,12 @@@ DEFAULT_SLICE = slice(None)  # for flak
  
  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 +243,9 @@@ def format_repos(repos: Tuple[Path, ...
  
  
  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.")
index 90d2047790b2ba2e916b4cae3e18518117334b01,543184e1cd4f373d3239cfdaf429c351e033b5eb..543184e1cd4f373d3239cfdaf429c351e033b5eb
  "  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."
      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 +50,25 @@@ if !exists("g:black_skip_string_normali
      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 +77,7 @@@
  \    'target_version=py37',
  \    'target_version=py38',
  \    'target_version=py39',
+ \    'target_version=py310',
  \  ]
  endfunction
  
index 73e19608108d5541a4d1e49081b1ef2e9207b829,8c55076e4c9331048f196affa4f40957d547663a..8c55076e4c9331048f196affa4f40957d547663a
  
  [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..107c6444dca348d368dc8ab35895fa14600e7490
@@@ -14,7 -14,7 +14,7 @@@ import sy
  
  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..0f42bafe334e5198235530c193c09fa7d75d8302
@@@ -8,7 -8,7 +8,7 @@@ import o
  import sys
  
  import commonmark
- from bs4 import BeautifulSoup
+ from bs4 import BeautifulSoup  # type: ignore[import]
  
  
  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__":
index 0000000000000000000000000000000000000000,895516deb51b03ca708c097dc7979e449eeca07a..895516deb51b03ca708c097dc7979e449eeca07a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,236 +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()
index a9ca8eff8b09b64bb70420add85dd0e890b4b60f,929d3eac4f57132dfb5c3e5364109a30052cbfa4..929d3eac4f57132dfb5c3e5364109a30052cbfa4
@@@ -8,7 -8,8 +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 +21,7 @@@
      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 +33,9 @@@
          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 +49,7 @@@
          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 +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()
index 0000000000000000000000000000000000000000,3c7cae60f7fbffd2a309fba37894eaeb0474607d..3c7cae60f7fbffd2a309fba37894eaeb0474607d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,66 +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()
index 0000000000000000000000000000000000000000,ff52939460ccc26e7f851720acf75600cea71641..ff52939460ccc26e7f851720acf75600cea71641
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,96 +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))
index fdbaf040d642b0a41d15d0f98112090971425643,188a4f79f0e51f027f445525bda3b2e68fd93081..188a4f79f0e51f027f445525bda3b2e68fd93081
@@@ -1,20 -1,16 +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,
      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 +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]
      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
  
              "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 +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",
      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(
          "(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",
      "--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(
      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(
  @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 "
      "-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",
          " 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,
      help="Read configuration from FILE path.",
  )
  @click.pass_context
- def main(
+ def main(  # noqa: C901
      ctx: click.Context,
      code: Optional[str],
      line_length: int,
      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__}`!"
          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:
              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,
                  report=report,
              )
          else:
+             from black.concurrency import reformat_many
              reformat_many(
                  sources=sources,
                  fast=fast,
              )
  
      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)
  
  def get_sources(
      *,
-     ctx: click.Context,
+     root: Path,
      src: Tuple[str, ...],
      quiet: bool,
      verbose: bool,
      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:
              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.
                  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(),
                  )
              )
          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 +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:
              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
              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:
          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,
      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)
          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 +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())
                  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 +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:
      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:
      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
      """
      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:
      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 +1020,9 @@@ def format_ipynb_string(src_contents: s
      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)
          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
          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 +1147,47 @@@ def decode_bytes(src: bytes) -> Tuple[F
          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:
                          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 +1343,16 @@@ def get_future_imports(node: Node) -> S
      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:
      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
          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 +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()
  
  
index 0000000000000000000000000000000000000000,f3304e48ed04935f184c49cb52cae07b232032fd..f3304e48ed04935f184c49cb52cae07b232032fd
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,478 +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..85dac6edd1eaa1e86a7c4f08f09d8a7aec4752dc
@@@ -1,20 -1,22 +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 +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 +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
          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 +344,32 @@@ def max_delimiter_priority_in_atom(node
  
      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..6baa096bacafccc22f66eecb2bbaee9ad3862b25
  """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..226968bff983f500d1f47ae60741e11e7cb10c80
@@@ -1,22 -1,29 +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 +107,7 @@@ def list_comments(prefix: str, *, is_en
  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.
          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 +170,11 @@@ def convert_one_fmt_off_pair(node: Node
                  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
                  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 +211,87 @@@ def generate_ignored_nodes(leaf: Leaf, 
      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`.
      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..55c96b66c86e14fb4fb4b2a8f5aef54aa0e34dd3
@@@ -1,9 -1,27 +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 +29,6 @@@
  
      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 +48,14 @@@ def cancel(tasks: Iterable["asyncio.Tas
  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.
          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..ee466679c7074caf936b1e2707b82468e8d83432
@@@ -1,4 -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..cebc48765babd1ea989f64cf88cbe094f8370a60
@@@ -1,12 -1,11 +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")
  @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..362898dc0fd71bdc1e19d5c589837c2d0c2a0c96
@@@ -1,9 -1,10 +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,
      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
  
      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())]
  
  
      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)
              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
      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"
  
  
  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.
  
      """
      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 +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,
  
      `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,
                  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..55ef2267df829f3b38de9ff5ce02f0d0a73edef3
@@@ -1,22 -1,21 +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 +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 +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]:
      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 +110,7 @@@ def put_trailing_semicolon_back(src: st
      """
      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 +217,9 @@@ def replace_cell_magics(src: str) -> Tu
      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 +297,28 @@@ def _is_ipython_magic(node: ast.expr) -
      )
  
  
+ 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.
  
  
      For example,
  
-         %%time\nfoo()
+         %%time\n
+         foo()
  
      would have been transformed to
  
      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."""
              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 +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.
  
      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.
          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)
              )
          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]}"
                  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..2bfe587fa0ec5120287d133c7371ee85716d5ec7
@@@ -1,35 -1,81 +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 +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.
  
      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.
              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
  
              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)
                  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.
          """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.
          `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)
  
          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)
              ):
          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."""
              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()
              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]
              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:
                          # 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)
  
          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] = ()
  
      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)
          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
              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
  
              # 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,
                  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
          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.
              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:
      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:
  
  def right_hand_split(
      line: Line,
-     line_length: int,
+     mode: Mode,
      features: Collection[Feature] = (),
      omit: Collection[LeafID] = (),
  ) -> Iterator[Line]:
  
      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] = []
      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"
                      " 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.
  
  
  
  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:
                      )
                      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:
                          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 +1048,39 @@@ def dont_increase_indentation(split_fun
      """
  
      @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
              )
              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):
                      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
                  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 +1214,9 @@@ def normalize_prefix(leaf: Leaf, *, ins
      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
          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 (
              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
  
              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 +1571,8 @@@ def generate_trailers_to_omit(line: Lin
                  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
                      )
                  ):
              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 +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
  
      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..6acc95e7a7e004dbf2e35a571bb08def6f1100fe
@@@ -1,9 -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,
      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 +59,9 @@@
      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
              # 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):
          """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?"""
              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?
      @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:
  
          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:
              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
              # 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
          """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
              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
          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
      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]
              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
              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
  
          ):
              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 +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:
  
  
  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
  
      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
              # 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..309f22dae94b61595bdc2e9d713ee887e3b75f27
@@@ -5,14 -5,16 +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
      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
      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,
          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 +165,37 @@@ def supports_feature(target_versions: S
      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)
      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 = "-"
              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..edd201a21e9f2311d3482c1183f1d100e1f073ef
@@@ -3,34 -3,24 +3,24 @@@ blib2to3 Node/Leaf transformation-relat
  """
  
  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 +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 +85,8 @@@ UNPACKING_PARENTS: Final = 
      syms.listmaker,
      syms.testlist_gexp,
      syms.testlist_star_expr,
+     syms.subject_expr,
+     syms.pattern,
  }
  TEST_DESCENDANTS: Final = {
      syms.test,
      syms.term,
      syms.power,
  }
+ TYPED_NAMES: Final = {syms.tname, syms.tname_star}
  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()`."""
  
                  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
                      # 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
          ):
              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
              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:
          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)
              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:
  
              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
  
      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 +441,6 @@@ def prev_siblings_are(node: Optional[LN
      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 +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 +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 +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 +720,11 @@@ def is_multiline_string(leaf: Leaf) -> 
  
  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
      ):
          return False
  
+     if node.children[3].prefix.strip():
+         return False
      return is_stub_body(node.children[2])
  
  
@@@ -743,7 -749,8 +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 +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 +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..67ac8595fcc6783ea2c6bf9de96d0013044ae496
@@@ -1,6 -1,7 +1,7 @@@
  """
  Formatting numeric literals.
  """
  from blib2to3.pytree import Leaf
  
  
@@@ -25,13 -26,10 +26,10 @@@ def format_scientific_notation(text: st
      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 +45,7 @@@ def format_float_or_int_string(text: st
  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 +54,8 @@@
          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..f4c17f28ea418d363f9b388e3770c208ca26b1b1
@@@ -4,13 -4,14 +4,14 @@@ The double calls are for patching purpo
  """
  
  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 +20,7 @@@
      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 +29,7 @@@
      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 +59,8 @@@ def diff(a: str, b: str, a_name: str, b
      """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 +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..ea282d1805cfea68ccf406e7e92e676de16f5fca
@@@ -1,33 -1,19 +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 +24,28 @@@ def get_grammars(target_versions: Set[T
      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 +56,10 @@@ def lib2to3_parse(src_txt: str, target_
      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
                  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):
      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)
  
  
  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
  
                  # 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..89899f2f38996f309f79dc1225e24bfa8c69767a
@@@ -1,13 -1,14 +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 +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..ebd4c052d1f37b9eed01d329143c15bfbe0f0e74
@@@ -2,8 -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..0d30f09ed112d28583c9579f82e6221087b5bb01
@@@ -2,12 -2,28 +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 +53,7 @@@ def lines_with_leading_tabs_expanded(s
      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 +144,32 @@@ def assert_is_leaf_string(string: str) 
      ), 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.
  
          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):
          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..a3f6467cc9e3920758ce3b5ddf00a5e2c41129b6
@@@ -1,18 -1,22 +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,
      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):
  # 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]:
      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
          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
              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.
                  " 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 +294,7 @@@ class CustomSplit
      break_idx: int
  
  
+ @trait
  class CustomSplitMapMixin:
      """
      This mixin class is used to map merged strings to a sequence of
      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":
  
          Returns:
              * A list of the custom splits that are mapped to @string, if any
-             exist.
-                 OR
+               exist.
+               OR
              * [], otherwise.
  
          Side Effects:
          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:
  
          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 leaf.type == token.STRING and "\\\n" in leaf.value:
-                 return Ok(i)
+                 if not is_part_of_annotation(leaf) and not contains_comment:
+                     string_indices.append(idx)
+                 # Advance to the next non-STRING leaf.
+                 idx += 2
+                 while is_valid_index(idx) and LL[idx].type == token.STRING:
+                     idx += 1
+             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
+             else:
+                 idx += 1
  
-         return TErr("This line has no strings that need merging.")
+         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_idx: int) -> Iterator[TResult[Line]]:
+     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()
  
  
      @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
          """
          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.
          """
  
          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
                  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]
              # 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)
  
  
              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)
          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.
                  - 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 +807,15 @@@ class StringParenStripper(StringTransfo
  
          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
                  }:
                      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):
  
      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,
          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
  
          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
              # 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
+         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))
  
- class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
+ 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:
          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
          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]
  
          # 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
          )
              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
              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}"
          # 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
              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():
                  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
              # 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.
  
              # 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)
          """
          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()
                  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
                  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)
          """
          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) :]
          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.
          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:
              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.
                              " 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.")
  
          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
          ):
              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
          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...
  
          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])
                      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 +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,
          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
          """
          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.
index cc966404a743a37bd2059c89732e7ad9d3b8b384,972f24181cb60e538e5237ea9597c9315758dc21..972f24181cb60e538e5237ea9597c9315758dc21
@@@ -1,13 -1,14 +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(
          + "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()
  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 +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 +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 +121,10 @@@ async def handle(request: web.Request, 
          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
              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 +211,8 @@@ def parse_python_variant_header(value: 
                      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
  def patched_main() -> None:
      maybe_install_uvloop()
      freeze_support()
-     black.patch_click()
      main()
  
  
index 0000000000000000000000000000000000000000,b5a4b137446f9ef12422e14079012b4cf1982a63..b5a4b137446f9ef12422e14079012b4cf1982a63
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,3 +1,3 @@@
+ import blackd
+ blackd.patched_main()
index 97994ecc1dff81df44d81076534c9886e527bf1f,370e0ae222eebfef1b73c185b4fc6130e87974cc..370e0ae222eebfef1b73c185b4fc6130e87974cc
@@@ -1,7 -1,18 +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 +42,4 @@@ def cors(allow_headers: Iterable[str]) 
  
          return resp
  
-     return impl  # type: ignore
+     return impl
index ac8a067378d71ec0ba4f05a2a2af5b2a68fbfad7,5db78723cecd4e4fdd474bb9a528448fc27ea4e4..5db78723cecd4e4fdd474bb9a528448fc27ea4e4
@@@ -12,11 -12,17 +12,17 @@@ file_input: (NEWLINE | stmt)* ENDMARKE
  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 +30,7 @@@
  #     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]]
  # 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 +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 +89,6 @@@ testlist_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 +107,23 @@@ import_as_names: import_as_name (',' im
  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 +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 +168,17 @@@ listmaker: (namedexpr_test|star_expr) 
  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)* [',']
  
  # 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 +226,31 @@@ encoding_decl: NAM
  
  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..38b04158ddb70070110e5047aab88bb9009ea003
@@@ -1,21 -1,24 +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..04eccfa1d4b6ac10b3d264b8fff0b494b8849549
@@@ -29,7 -29,7 +29,7 @@@ without having to invoke the Python pge
  """
  
  # Python imports
- import regex as re
+ import re
  
  # Local imports
  from pgen2 import grammar, token
@@@ -63,7 -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 +72,7 @@@
              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)
          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..e629843f8b986ea792d59a150ff64018d2108bcb
@@@ -17,54 -17,115 +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)
              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
                  )
              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
          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
          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 = ""
  
  
  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,
          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
  
  
  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)
      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..1f3fdc55b972e1c75d0e4bca952c2c0b0d86fece
@@@ -16,19 -16,19 +16,19 @@@ fallback token code OP, but the parser 
  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
          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
              "number2symbol",
              "dfas",
              "keywords",
+             "soft_keywords",
              "tokens",
              "symbol2label",
          ):
          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..53c0b8ac2bbb2984a8b78d03261d8c7ebd0eb5ca
@@@ -4,11 -4,9 +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 +20,7 @@@
  }
  
  
- 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 +42,7 @@@
      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..ad51a3dad08dfbbe1fee7cc9b7845eeb7cb263fd
@@@ -9,24 -9,32 +9,32 @@@ See Parser/parser.c in the Python distr
  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 +45,97 @@@ def lam_sub(grammar: Grammar, node: Raw
      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
          self.context = context
  
  
- class Parser(object):
+ class Parser:
      """Parser engine.
  
      The proper usage sequence is:
          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
  
          """
          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.
          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]
              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
                          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
                      # 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..3ece9bb41edd51c7d2d82a317cb14dafcea4f1f4
@@@ -1,26 -1,22 +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 +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 +71,7 @@@
          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 +81,7 @@@
              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():
              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)
                  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]
                  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
              # 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:
                  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):
                  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 "")
              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
          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)
          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..ed2fc4e85fce976b972d7f69b038cb2270100670
@@@ -1,12 -1,6 +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 +72,7 @@@ NT_OFFSET: Final = 25
  
  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..d0607f4b1e17b202d4d19603287ed5cfc0a57288
@@@ -27,27 -27,44 +27,44 @@@ are the same, except instead of generat
  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 +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 +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 +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 +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().
  
  
  # 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
  
          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)
                  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
  
  
  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 +347,7 @@@ def detect_encoding(readline: Callable[
          try:
              return readline()
          except StopIteration:
-             return bytes()
+             return b""
  
      def find_cookie(line: bytes) -> Optional[str]:
          try:
      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
  
  
  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
      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]
      # `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
              line = readline()
          except StopIteration:
              line = ""
-         lnum = lnum + 1
+         lnum += 1
          pos, max = 0, len(line)
  
          if contstr:  # continued string
              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
  
                      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)
                  ):
                      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
  
                      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]
                      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..2b43b4c112b15c3c508767d6681129457d8a2805
@@@ -5,13 -5,10 +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 +18,7 @@@
  #                                      "PatternGrammar.txt")
  
  
- class Symbols(object):
+ class Symbols:
      def __init__(self, grammar: Grammar) -> None:
          """Initializer.
  
@@@ -39,12 -36,14 +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 +63,6 @@@
      encoding_decl: int
      eval_input: int
      except_clause: int
-     exec_stmt: int
      expr: int
      expr_stmt: int
      exprlist: int
@@@ -74,6 -72,7 +72,7 @@@
      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 +81,7 @@@
      import_stmt: int
      lambdef: int
      listmaker: int
+     match_stmt: int
      namedexpr_test: int
      not_test: int
      old_comp_for: int
      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
      single_input: int
      sliceop: int
      small_stmt: int
+     subject_expr: int
      star_expr: int
      stmt: int
      subscript: int
      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 +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
          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..2a0cd6d196a3410536b4e9a07d5ded47d6c90b54
@@@ -10,22 -10,21 +10,21 @@@ even the comments and whitespace betwee
  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 +34,10 @@@ from io import StringI
  
  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
      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.
  
              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:
          """
          raise NotImplementedError
  
+     def __deepcopy__(self: _P, memo: Any) -> _P:
+         return self.clone()
      def clone(self: _P) -> _P:
          """
          Return a cloned (deep) copy of self.
              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
  
  
  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:
          """
          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.
  
          """
          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)
  
              yield from child.pre_order()
  
      @property
-     def prefix(self) -> Text:
+     def prefix(self) -> str:
          """
          The whitespace and comments preceding this node in the input.
          """
          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
  
  
  
  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.
              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)
  
          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 +500,10 @@@ def convert(gr: Grammar, raw_node: RawN
          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.
  
      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 +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.
          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
  
  
  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.
              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:
  
  
  class WildcardPattern(BasePattern):
      """
      A wildcard pattern can match zero or more nodes.
  
  
      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.
  
  
  class NegatedPattern(BasePattern):
-     def __init__(self, content: Optional[Any] = None) -> None:
+     def __init__(self, content: Optional[BasePattern] = None) -> None:
          """
          Initializer.
  
          # 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 +981,3 @@@ def generate_matches
                      r.update(r0)
                      r.update(r1)
                      yield c0 + c1, r
- _Convert = Callable[[Grammar, RawNode], Any]
index 5bc494d599966e2631f37c79d180e260c7f746be,a3d262bc53d9bd1b3463cc76e13858b2c7dc3616..a3d262bc53d9bd1b3463cc76e13858b2c7dc3616
@@@ -1,6 -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..1a0dd747d8ecc3e422cf47a150943b60ad24d692
@@@ -1,1 -1,28 +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")
index 0000000000000000000000000000000000000000,7c16bdfb3a52bd5ff45ffd89fe430504fe40d3c6..7c16bdfb3a52bd5ff45ffd89fe430504fe40d3c6
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,47 +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)
index 4eea013151a1f205fd4ae074b35293dc686014ae,1487dc4b6e2c546a8952fef91315cb57b063e29d..1487dc4b6e2c546a8952fef91315cb57b063e29d
@@@ -154,6 -154,9 +154,9 @@@ class Test
                  not parsed.hostname.strip()):
              pass
  
+ a = "type comment with trailing space"  # type: str   
  #######################
  ### SECTION COMMENT ###
  #######################
@@@ -226,6 -229,7 +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 +335,8 @@@ class Test
              pass
  
  
+ a = "type comment with trailing space"  # type: str
  #######################
  ### SECTION COMMENT ###
  #######################
index fbbef6dcc6bdcf7657d59284b3b9c335116b1670,f964bee66517c592afb03dbdeb317b3b40dd8552..f964bee66517c592afb03dbdeb317b3b40dd8552
@@@ -1,6 -1,7 +1,7 @@@
  # The percent-percent comments are Spyder IDE cells.
  
- #%%
+ # %%
  def func():
      x = """
      a really long string
@@@ -44,4 -45,4 +45,4 @@@
      )
  
  
- #%%
+ # %%
index 2147d41c9da74bcd6df291509911740f95393319,9f4f39d83599dde87d3f720ea3b4cbbc3a33988c..9f4f39d83599dde87d3f720ea3b4cbbc3a33988c
@@@ -85,7 -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))
index d83b6b8ff47cc6f731aee0f7a3369f3d6e7db960,bda40619f62ce28ea7259d78a53403acdc237359..bda40619f62ce28ea7259d78a53403acdc237359
@@@ -58,10 -58,12 +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():
      ...
index 0000000000000000000000000000000000000000,a2030c2a092628e61e25e467baef7ffa6581a3e2..a2030c2a092628e61e25e467baef7ffa6581a3e2
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,15 +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.
+ # %%
+ # %%
index 0000000000000000000000000000000000000000,77b25556e74aae2dc4a02e4e32f2b7aac460d484..77b25556e74aae2dc4a02e4e32f2b7aac460d484
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,305 +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
index 0000000000000000000000000000000000000000,c30cd76c791855c7670ce2f4284b2ee454bab707..c30cd76c791855c7670ce2f4284b2ee454bab707
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,161 +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
+     )
index 96bcf525b16fe56538739f4c895e021a4766dadd,c31d6a68783e3d904186c292c3e933986f471c8d..c31d6a68783e3d904186c292c3e933986f471c8d
@@@ -173,6 -173,11 +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 +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 +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 +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>
+     """
index 0000000000000000000000000000000000000000,6fea860adf6aab2a3b5ec6ae61d954a07fe7c77d..6fea860adf6aab2a3b5ec6ae61d954a07fe7c77d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,4 +1,4 @@@
+ # Make sure when the file ends with class's docstring,
+ # It doesn't add extra blank lines.
+ class ClassWithDocstring:
+     """A docstring."""
index a90b578f09afb71ddff8e84e8ede8dcc5372fe1d,4ec6b8a01535a17d092a4ac0d255c43d8ec524d2..4ec6b8a01535a17d092a4ac0d255c43d8ec524d2
@@@ -1,3 -1,4 +1,4 @@@
+ # flags: --skip-string-normalization
  class ALonelyClass:
      '''
      A multiline class docstring.
index 0000000000000000000000000000000000000000,ff4819acb67a876c50377bcc535f179332c6aed7..ff4819acb67a876c50377bcc535f179332c6aed7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,103 +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)."
index 0000000000000000000000000000000000000000,712c7364f51bd97d73b1c1b8f0bd82e6274ba913..712c7364f51bd97d73b1c1b8f0bd82e6274ba913
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,11 +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.'''
index 4c03e432383c174127c803dea8076488a09a8daf,4fd47b93dcacaec67765c8129171f8f07a8523f8..4fd47b93dcacaec67765c8129171f8f07a8523f8
@@@ -119,7 -119,6 +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:
index 721a07d2141ab71fc2d898302e7e7eccf44bd276,2eaaeb479f8c7334e75468642c01fa25c5d92f9c..2eaaeb479f8c7334e75468642c01fa25c5d92f9c
   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 +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
  +(
index d13450cda68e81beba962c42e4c14d4c0f0938eb,06096c589f1791f9389f07b6ecef0f6b88299580..06096c589f1791f9389f07b6ecef0f6b88299580
@@@ -282,15 -282,15 +282,15 @@@ Name1 or Name2 and Name3 or Name
  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 +347,13 @@@ str or None if (1 if True else 2) else 
      *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 +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 +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 +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 (
      | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h
      ^ aaaaaaaaaaaaaaaa.i
      << aaaaaaaaaaaaaaaa.k
-     >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
+     >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
  ):
      return True
  (
index 5a50eb12ed32b21b31c836505dc6e94ffea2fd56,d1f15cd5c8b682c72de85e0a2e0b8d877ffe8730..d1f15cd5c8b682c72de85e0a2e0b8d877ffe8730
@@@ -195,7 -195,6 +195,6 @@@ import sy
  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 +204,7 @@@ f"trigger 3.6 mode
  
  # Comment 2
  
  # fmt: off
  def func_no_args():
    a; b; c
index 0000000000000000000000000000000000000000,181151b6bd6114d677b233e363c9606315e877da..181151b6bd6114d677b233e363c9606315e877da
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,176 +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()
index 0000000000000000000000000000000000000000,8b3c0bc662a45bda119ebeb21c2b68e2b68ef733..8b3c0bc662a45bda119ebeb21c2b68e2b68ef733
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,19 +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
index 0000000000000000000000000000000000000000,15ac0ad7080eb8e212e323f03854707c350531f9..15ac0ad7080eb8e212e323f03854707c350531f9
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,11 +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
index 0000000000000000000000000000000000000000,38e9c2a9f47b99acbe8e710ddae9eb675a94797c..38e9c2a9f47b99acbe8e710ddae9eb675a94797c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,62 +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")
index 4b33231c01c08701c1f2fa47de9cfe8a1f0388c9,60560309376351e79d53d505dd3f89b1745eae7b..60560309376351e79d53d505dd3f89b1745eae7b
@@@ -7,6 -7,8 +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 +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}"
index 0000000000000000000000000000000000000000,9b9b9c673de63917b1ec3ec7d137dce7ede97302..9b9b9c673de63917b1ec3ec7d137dce7ede97302
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,301 +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],
+ ]: ...
index cfc259ea7bd7be7e645c606c4e6caed225007ed2,5bb36c26318cbd0b9992b55fd73da3485cf500c7..5bb36c26318cbd0b9992b55fd73da3485cf500c7
@@@ -23,6 -23,35 +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 +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
index 02078219e8255f00559ac63391af8394bab63115,92f46e275160800bf00af9a8f95b3bfb4cc22a1e..92f46e275160800bf00af9a8f95b3bfb4cc22a1e
@@@ -49,6 -49,17 +49,17 @@@ def func() -> ((also_super_long_type_an
  ):
      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 +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 +140,7 @@@ def some_function_with_a_really_long_na
  
  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 +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,
+ )
index 0000000000000000000000000000000000000000,4fae7530eb98ab918766980229dc88320c83a8e0..4fae7530eb98ab918766980229dc88320c83a8e0
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,42 +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
index 0000000000000000000000000000000000000000,158038bf960607aa048e6cde702c8a4ce6c0bb62..158038bf960607aa048e6cde702c8a4ce6c0bb62
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,6 +1,6 @@@
+ # flags: --line-length=6
+ # Regression test for #3427, which reproes only with line length <= 6
+ def f():
+     """
+     x
+     """
index ef3094fd77969401ff8f99067f61e3ec769fc2fc,db3954e3abd7ea472f3e090c11f5839f0bcc5c6c..db3954e3abd7ea472f3e090c11f5839f0bcc5c6c
@@@ -133,11 -133,14 +133,14 @@@ old_fmt_string2 = "This is a %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."
index 0000000000000000000000000000000000000000,d5897b4db60e7fbd6adf309caef9528749f389b1..d5897b4db60e7fbd6adf309caef9528749f389b1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,26 +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
index 0000000000000000000000000000000000000000,e1f81b4d76b5a3c85044d6a4ce8de67c77932e3d..e1f81b4d76b5a3c85044d6a4ce8de67c77932e3d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,68 +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
index 0000000000000000000000000000000000000000,0631e136a3d85f8de815ea34b7e57de914aea1da..0631e136a3d85f8de815ea34b7e57de914aea1da
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,8 +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
index 0000000000000000000000000000000000000000,515174dcc04fcd78b40b96986dbcbe6d46fcef2f..515174dcc04fcd78b40b96986dbcbe6d46fcef2f
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,9 +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
index 0000000000000000000000000000000000000000,6ec8bb454088d5707c7db4b61e223655c1f695db..6ec8bb454088d5707c7db4b61e223655c1f695db
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,41 +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
index 0000000000000000000000000000000000000000,b81549ec115c9bec521cb606d5ec9d91dc2924f2..b81549ec115c9bec521cb606d5ec9d91dc2924f2
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,44 +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: ...
index 254da68d3308bf76cdf464dbf9aa5a5a74252828,996693287448580093d4be201fc770b6a985308c..996693287448580093d4be201fc770b6a985308c
@@@ -1,5 -1,3 +1,3 @@@
- #!/usr/bin/env python3.6
  x = 123456789
  x = 123456
  x = .1
@@@ -21,9 -19,6 +19,6 @@@ x = 13333
  
  # output
  
- #!/usr/bin/env python3.6
  x = 123456789
  x = 123456
  x = 0.1
index e345bb90276c709939125f2e366f45318e973486,6d60bdbb34d185dd63a79b1fcf579b8183ee78dc..6d60bdbb34d185dd63a79b1fcf579b8183ee78dc
@@@ -1,5 -1,3 +1,3 @@@
- #!/usr/bin/env python3.6
  x = 123456789
  x = 1_2_3_4_5_6_7
  x = 1E+1
@@@ -11,8 -9,6 +9,6 @@@ x = 1_2
  
  # output
  
- #!/usr/bin/env python3.6
  x = 123456789
  x = 1_2_3_4_5_6_7
  x = 1e1
index 0000000000000000000000000000000000000000,39205ba9f7aae25cafd20b132920bcb676420a57..39205ba9f7aae25cafd20b132920bcb676420a57
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,36 +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,],
+ ]
index 0000000000000000000000000000000000000000,16645a18baa25b65deb728300c91e44ae92a5c55..16645a18baa25b65deb728300c91e44ae92a5c55
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,46 +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,
+ ):
+     ...
index 0000000000000000000000000000000000000000,10b4d26e28969defb2b5c22daae726d61a1ff64e..10b4d26e28969defb2b5c22daae726d61a1ff64e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,149 +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
index 0000000000000000000000000000000000000000,1e1481d7bbecda80bd9b037764cc086cc8bad4dd..1e1481d7bbecda80bd9b037764cc086cc8bad4dd
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,120 +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
index 0000000000000000000000000000000000000000,4b4d45f0bffb5966e63225efb87a220bad9f8e0d..4b4d45f0bffb5966e63225efb87a220bad9f8e0d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,108 +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}"
index 0000000000000000000000000000000000000000,6fa2000f0dedc93db0837093443e98376ac78025..6fa2000f0dedc93db0837093443e98376ac78025
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,93 +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")
index 0000000000000000000000000000000000000000,2ee6ea2b6e99b5d168227f9803386e92f64f6c9d..2ee6ea2b6e99b5d168227f9803386e92f64f6c9d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,92 +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
index 0000000000000000000000000000000000000000,fee2b8404943cdbd0ba1592f0646695270d3581a..fee2b8404943cdbd0ba1592f0646695270d3581a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,188 +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,
+ ): ...
index ca8f7ab1d95d08f642db68a852f2821c09a9c720,2641c2b970edb77adb82a1fbeaad3fd48bbf3e63..2641c2b970edb77adb82a1fbeaad3fd48bbf3e63
@@@ -1,3 -1,4 +1,4 @@@
+ # flags: --minimum-version=3.8
  def positional_only_arg(a, /):
      pass
  
index c6867f2625855693197e2d262120193788528b7c,742b6d5b7e41eed7d047cfa2c5ec5d221c59358c..742b6d5b7e41eed7d047cfa2c5ec5d221c59358c
@@@ -1,10 -1,11 +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))
index 20e80a693774a50efb6f969fe6de3f3eeda6370f,08dba3ffdf9b3eacc1c4410452bd631f06085384..08dba3ffdf9b3eacc1c4410452bd631f06085384
@@@ -1,3 -1,4 +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 +20,7 @@@ with (y := [3, 2, 1]) as (funfunfun := 
  @(please := stop)
  def sigh():
      pass
+ for (x := 3, y := 4) in y:
+     pass
index 0000000000000000000000000000000000000000,9f999deeb89effc38301cf682d19abedcce38f95..9f999deeb89effc38301cf682d19abedcce38f95
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,16 +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)
index 7bbd509119729979974c46e164aee0ea43dcb1bc,d1614624d99448a5e6ea7b6a02a518da65280755..d1614624d99448a5e6ea7b6a02a518da65280755
@@@ -1,7 -1,8 +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)]
index 9718d95b499de4e2c966de31d10433deb471e681,24f1ac29168f7069e8d3563080522bf80ffe1984..24f1ac29168f7069e8d3563080522bf80ffe1984
@@@ -1,3 -1,4 +1,4 @@@
+ # flags: --minimum-version=3.8
  if (foo := 0):
      pass
  
@@@ -49,6 -50,25 +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 +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
index 0000000000000000000000000000000000000000,b68d59d6440785756af07c118a9d5cbee2ce615d..b68d59d6440785756af07c118a9d5cbee2ce615d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,25 +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
index 0000000000000000000000000000000000000000,92b568a379c4202aae7a45e5f0e46bc126eb408d..92b568a379c4202aae7a45e5f0e46bc126eb408d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,195 +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__
index 0000000000000000000000000000000000000000,12e49180e415f8ea07e81fbc8d99e7eb16ce3e4a..12e49180e415f8ea07e81fbc8d99e7eb16ce3e4a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,54 +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
index 0000000000000000000000000000000000000000,0d34650e0980fabfd2ed63cf52df37ce818219d9..0d34650e0980fabfd2ed63cf52df37ce818219d9
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,112 +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
index 0000000000000000000000000000000000000000,d9b31403c9d639d27cb5addaa7491624710a1301..d9b31403c9d639d27cb5addaa7491624710a1301
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,11 +1,11 @@@
+ # flags: --line-length=0
+ importA;()<<0**0#
+ # output
+ importA
+ (
+     ()
+     << 0
+     ** 0
+ )  #
index 0000000000000000000000000000000000000000,b3ef0aae08462ee21c6cf1e6e9ae7193cef62fa8..b3ef0aae08462ee21c6cf1e6e9ae7193cef62fa8
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,149 +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
+ )
index 0000000000000000000000000000000000000000,e15e5ddc86d0d3fd445a078c6b35d88b05dbcc59..e15e5ddc86d0d3fd445a078c6b35d88b05dbcc59
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,38 +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
index 0000000000000000000000000000000000000000,0a7671be5a6edb0b362123b1237211689bd02422..0a7671be5a6edb0b362123b1237211689bd02422
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,28 +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
index 0849374f776ebbefc13f0332dd55aaeb0e690feb,d5da6654f0c2503acf74844f69e3e0b9b0c1bb26..d5da6654f0c2503acf74844f69e3e0b9b0c1bb26
@@@ -1,3 -1,4 +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
index ca9d7c62b215515e57c21a0ef55dd8ad07174c36,006d4f7266f8e872ba41cae9f5d6dbe95b315421..006d4f7266f8e872ba41cae9f5d6dbe95b315421
@@@ -1,3 -1,4 +1,4 @@@
+ # flags: --preview
  from .config import (
      Any,
      Bool,
@@@ -131,6 -132,18 +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 +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
+     ),
+ ]
index 0000000000000000000000000000000000000000,719d94fdcc541cb9416a332f32e7ab0c138ad370..719d94fdcc541cb9416a332f32e7ab0c138ad370
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,55 +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
index 0000000000000000000000000000000000000000,589e00ad187232bf4a1e0d9e089414350bfeb71f..589e00ad187232bf4a1e0d9e089414350bfeb71f
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,175 +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
index 0000000000000000000000000000000000000000,a9e31076f03132958eec3879665f52f5f1b3530c..a9e31076f03132958eec3879665f52f5f1b3530c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,36 +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
index 0000000000000000000000000000000000000000,af1e83fe74c4d0c05e5f3fabb7c31c20bad6e28b..af1e83fe74c4d0c05e5f3fabb7c31c20bad6e28b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,38 +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
index 0000000000000000000000000000000000000000,25217a406042c864dc9fdc4adc8dc2558e72147d..25217a406042c864dc9fdc4adc8dc2558e72147d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,47 +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
index 0000000000000000000000000000000000000000,3f72e48db9d5d876224d8e2230cfffe5d150f26a..3f72e48db9d5d876224d8e2230cfffe5d150f26a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,35 +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
index 0000000000000000000000000000000000000000,98b69bf87b230ed5886e19462848689f9d460960..98b69bf87b230ed5886e19462848689f9d460960
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,100 +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
index 0000000000000000000000000000000000000000,65c3d8d166edd5ce8f4f69a6745ad2a345e4acca..65c3d8d166edd5ce8f4f69a6745ad2a345e4acca
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,34 +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"
index 0000000000000000000000000000000000000000,fbbacd13d1d959c288d434e485fa5ee52d5a5cc1..fbbacd13d1d959c288d434e485fa5ee52d5a5cc1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,91 +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,
+                             }
+                         )
+                     )
+                 )
+             )
+         }
+     ),
+ }
index 430f760cf0b9dc9a8490974198904b4ff01195ba,5519f0987741967c27c60adc231c018f1b6cfbac..5519f0987741967c27c60adc231c018f1b6cfbac
@@@ -1,3 -1,4 +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 +19,26 @@@ D3 = {x: "This is a really long string 
  
  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 +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 +130,7 @@@ fstring_with_no_fexprs = f"Some regula
  
  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 +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 +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 +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 +611,7 @@@ comment_string = (  # This comment get
  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 +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))
index 0000000000000000000000000000000000000000,d190f422a60bf3f119016d13fa1d0fc6d34333eb..d190f422a60bf3f119016d13fa1d0fc6d34333eb
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,26 +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
index 2bc0b6ed32885a7bac4c816d6bcd3c3e4ce6fcca,a8e8971968cffff7a5e4d355bc2ef8135b92702e..a8e8971968cffff7a5e4d355bc2ef8135b92702e
@@@ -1,3 -1,4 +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?"
index 36f323e04d6de18e7ca0041200951706c001b695,436157f4e0584b949c109db62b3a0bad3fb97cf0..436157f4e0584b949c109db62b3a0bad3fb97cf0
@@@ -1,3 -1,4 +1,4 @@@
+ # flags: --preview
  class A:
      def foo():
          result = type(message)("")
@@@ -209,8 -210,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 +525,42 @@@ xxxxxx_xxx_xxxx_xx_xxxxx_xxxxxxxx_xxxxx
      },
  )
  
+ # 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 +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 +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 +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 +1007,9 @@@ class xxxxxxxxxxxxxxxxxxxxx(xxxx.xxxxxx
          )
  
  
- 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 +1209,42 @@@ xxxxxx_xxx_xxxx_xx_xxxxx_xxxxxxxx_xxxxx
          ),
      },
  )
+ # 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']}'"
+ )
index 0000000000000000000000000000000000000000,8beb877bdd1e02bbc07e0ea723dcd351715d7955..8beb877bdd1e02bbc07e0ea723dcd351715d7955
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,60 +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
index 0000000000000000000000000000000000000000,3ff643610b7d50cd190f9b17056e8b28f985ae5f..3ff643610b7d50cd190f9b17056e8b28f985ae5f
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,387 +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
index 0000000000000000000000000000000000000000,303035a7efbdad5275d8b84025484d7fc63f9fe5..303035a7efbdad5275d8b84025484d7fc63f9fe5
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,59 +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...
+     """
index 0000000000000000000000000000000000000000,8e801ff6cdc3df0f6ac51582a2936a634ab8bc36..8e801ff6cdc3df0f6ac51582a2936a634ab8bc36
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,7 +1,7 @@@
+ # flags: --preview
+ x[(a:=0):]
+ x[:(a:=0)]
+ # output
+ x[(a := 0):]
+ x[:(a := 0)]
index b895443fb46bbf372727bef666a1c3e23deaa536,aeaf450ff5ea4b419654a9983bc196153fe5c6d7..aeaf450ff5ea4b419654a9983bc196153fe5c6d7
@@@ -1,3 -1,4 +1,4 @@@
+ # flags: --preview
  ("" % a) ** 2
  ("" % a)[0]
  ("" % a)()
index 0000000000000000000000000000000000000000,650c6fecb2044e0af61f163ef81f85c76f8d5dc9..650c6fecb2044e0af61f163ef81f85c76f8d5dc9
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,97 +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
index 0000000000000000000000000000000000000000,c732c33b53a8f3d36ef0c71888d0acd1d94608fd..c732c33b53a8f3d36ef0c71888d0acd1d94608fd
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,86 +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
+ )
index 0000000000000000000000000000000000000000,fea0ea6839a6574f2472a390ae876014d18d3da9..fea0ea6839a6574f2472a390ae876014d18d3da9
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,24 +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
index 0000000000000000000000000000000000000000,bba7e7ad16d29bc9a76e0be0ddc341bace1f2d79..bba7e7ad16d29bc9a76e0be0ddc341bace1f2d79
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,56 +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
+ }
index 0000000000000000000000000000000000000000,172be3898d6612fb71f64bea6315c30a68497a8f..172be3898d6612fb71f64bea6315c30a68497a8f
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,13 +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]
index dab8b404a739c57f5258694e28ebe65bd9714462,3f61106c45dc96a8731bac308b0cdb5676ffb8a4..3f61106c45dc96a8731bac308b0cdb5676ffb8a4
@@@ -1,4 -1,4 +1,4 @@@
- #!/usr/bin/env python3.7
+ # flags: --minimum-version=3.7
  
  
  def f():
@@@ -33,9 -33,6 +33,6 @@@ def make_arange(n)
  # output
  
  
- #!/usr/bin/env python3.7
  def f():
      return (i * 2 async for i in arange(42))
  
index 63b0588bc27a1affe2d5a955537728ad8f95f5be,919ea6aeed475f0bc27ba47e90c8e1d94f0f314b..919ea6aeed475f0bc27ba47e90c8e1d94f0f314b
@@@ -1,4 -1,4 +1,4 @@@
- #!/usr/bin/env python3.8
+ # flags: --minimum-version=3.8
  
  
  def starred_return():
@@@ -22,9 -22,6 +22,6 @@@ def t()
  # output
  
  
- #!/usr/bin/env python3.8
  def starred_return():
      my_list = ["value2", "value3"]
      return "value1", *my_list
index ae67c2257ebba5802cad6b259ca798574dd15515,1b9536c1529314c5ac26fd5ed3af1b8d98f658ea..1b9536c1529314c5ac26fd5ed3af1b8d98f658ea
@@@ -1,4 -1,4 +1,4 @@@
- #!/usr/bin/env python3.9
+ # flags: --minimum-version=3.9
  
  @relaxed_decorator[0]
  def f():
@@@ -14,10 -14,6 +14,6 @@@ def f()
  
  # output
  
- #!/usr/bin/env python3.9
  @relaxed_decorator[0]
  def f():
      ...
index 0000000000000000000000000000000000000000,8c7223d2f3900b7a6e0778665464f9df272a425c..8c7223d2f3900b7a6e0778665464f9df272a425c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,176 +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)
index 0000000000000000000000000000000000000000,322c5b7a51b07e74257a989d6668044f54988bce..322c5b7a51b07e74257a989d6668044f54988bce
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,79 +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
index 0000000000000000000000000000000000000000,cd5340462da800a429fa080a3d3037a471cff975..cd5340462da800a429fa080a3d3037a471cff975
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,48 +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)
index 0000000000000000000000000000000000000000,ef2e5c2f6f53fbb02b12191e9f12c3d53aa73721..ef2e5c2f6f53fbb02b12191e9f12c3d53aa73721
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,189 +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())
index 0000000000000000000000000000000000000000,fe6592b664d84ae3fe51d6cbb6f83c405d7a8900..fe6592b664d84ae3fe51d6cbb6f83c405d7a8900
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,35 +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"
index 0000000000000000000000000000000000000000,3ee64902a3028bf6d9f04a06de43ebf389e3eab7..3ee64902a3028bf6d9f04a06de43ebf389e3eab7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,120 +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:
+     ...
index 0000000000000000000000000000000000000000,8509ecdb92c0afd94d92d7d65393ad7332d2e978..8509ecdb92c0afd94d92d7d65393ad7332d2e978
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,223 +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
index 0000000000000000000000000000000000000000,4dda5df40f0f252cfa4937efc5caa5e1b1bb5547..4dda5df40f0f252cfa4937efc5caa5e1b1bb5547
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,75 +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)
index 7a42678f646fcfc523d82f3e1e3b5173b7daeceb,165117cdcb495be4470d8cbe41bb4c584432380c..165117cdcb495be4470d8cbe41bb4c584432380c
@@@ -9,7 -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::]
index 0000000000000000000000000000000000000000,13e517816d65fd63401228b0832c47dac07e854b..13e517816d65fd63401228b0832c47dac07e854b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,28 +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)
index 9ddc2b540fcefe3c729e198cdc9e6ba5da5f4861,f86da696e1566482850877c8ca3d1ed745ed0647..f86da696e1566482850877c8ca3d1ed745ed0647
@@@ -1,10 -1,13 +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 +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():
index 0000000000000000000000000000000000000000,f3828d55ba2b302c4521ac238a1dbb96d897667b..f3828d55ba2b302c4521ac238a1dbb96d897667b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,152 +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): ...
index 0000000000000000000000000000000000000000,2a194759a821ecb1b8529dc71cd5d6363a2e69d9..2a194759a821ecb1b8529dc71cd5d6363a2e69d9
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,91 +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"}
index 0000000000000000000000000000000000000000,85aa8badb261652e60427d43a5796d42e2e72bec..85aa8badb261652e60427d43a5796d42e2e72bec
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,63 +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
index 0000000000000000000000000000000000000000,9541670e3945df95d264f18e1176f38244c6f8cf..9541670e3945df95d264f18e1176f38244c6f8cf
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,12 +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
index 0000000000000000000000000000000000000000,c0ed699e6a61c2db737fefdcf727ef58941a6b33..c0ed699e6a61c2db737fefdcf727ef58941a6b33
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,21 +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}
index 0000000000000000000000000000000000000000,99d82a677f8359041c030d8b4cadb2f89f966693..99d82a677f8359041c030d8b4cadb2f89f966693
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,88 +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"
+ )
index 366a92fa9d4a8e860ee6a5780fc672a2e3fb98aa,ad8b610859031ec17b2d2a3d40b53b53f27eb4a3..ad8b610859031ec17b2d2a3d40b53b53f27eb4a3
@@@ -4,3 -4,6 +4,6 @@@
  x󠄀 = 4
  មុ = 1
  Q̇_per_meter = 4
+ A᧚ = 3
+ A፩ = 8
index 0000000000000000000000000000000000000000,7c2009e8202cf9a2820c69d485c36dbd2d1d4551..7c2009e8202cf9a2820c69d485c36dbd2d1d4551
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,30 +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))
index 0000000000000000000000000000000000000000,2e5ca2ede8c9ec1bd63ccc8813e9ad3ed30cd7a9..2e5ca2ede8c9ec1bd63ccc8813e9ad3ed30cd7a9
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,11 +1,11 @@@
+ def foo(
+     # type: Foo
+     x): pass
+ # output
+ def foo(
+     # type: Foo
+     x,
+ ):
+     pass
index 0000000000000000000000000000000000000000,720a775ef31bd8ef633fbc47d1e524c63207fdca..720a775ef31bd8ef633fbc47d1e524c63207fdca
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,58 +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
index 0000000000000000000000000000000000000000,a319c0117b19e2d198575c42faf95d6af928ae1b..a319c0117b19e2d198575c42faf95d6af928ae1b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,6 +1,6 @@@
+               
+            
+ # output
index 0000000000000000000000000000000000000000,2987e7bb646d5d283f6fd130593dda8e2bfc8e51..2987e7bb646d5d283f6fd130593dda8e2bfc8e51
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1 +1,1 @@@
+ a.py
index 0000000000000000000000000000000000000000,e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
mode 000000,100644..100644
--- /dev/null
index 0000000000000000000000000000000000000000,e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
mode 000000,100644..100644
--- /dev/null
index 0000000000000000000000000000000000000000,e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
mode 000000,100644..100644
--- /dev/null
index 0000000000000000000000000000000000000000,e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
mode 000000,100644..100644
--- /dev/null
index 0000000000000000000000000000000000000000,150f68c80f52952b9c2bf25a4348084aee22b2d1..150f68c80f52952b9c2bf25a4348084aee22b2d1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1 +1,1 @@@
+ */*
index 0000000000000000000000000000000000000000,e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
mode 000000,100644..100644
--- /dev/null
index 0000000000000000000000000000000000000000,e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
mode 000000,100644..100644
--- /dev/null
index 4a8a95c72371b70213f8eb65589aada239ca1414,d17467b15c71bfa9ea267ef6ae0f8d7b8ca38e03..d17467b15c71bfa9ea267ef6ae0f8d7b8ca38e03
   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 +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
  +(
index cad935e525a46057349becfcc1845ba37b2b28e3,4c9b70336e748ecc7c0384338ee7606b6496c95f..4c9b70336e748ecc7c0384338ee7606b6496c95f
@@@ -1,6 -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 +13,4 @@@ def long_function_name
      argument_six,
      *rest,
  ):
-     ...
+     pass
index 07ed93c6879363f22cde9de9e3c3c393aa2d41a1,40caf30a9831ded54ce91b8d0d4932d07a9c517a..40caf30a9831ded54ce91b8d0d4932d07a9c517a
@@@ -1,3 -1,4 +1,4 @@@
+ # flags: --pyi
  from typing import Union
  
  @bird
index 0000000000000000000000000000000000000000,fb49e2f93e725e67d7895800e200c383d72ba7f7..fb49e2f93e725e67d7895800e200c383d72ba7f7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,2 +1,2 @@@
+ This is not valid Python syntax
+ y = "This is valid syntax"
index 0000000000000000000000000000000000000000,22b5b94c0a46a78e9cc96c7444d902a05e16df71..22b5b94c0a46a78e9cc96c7444d902a05e16df71
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,18 +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
index 0000000000000000000000000000000000000000,8de2bb58adc8e8c833bf79829410afa3a9b32c6e..8de2bb58adc8e8c833bf79829410afa3a9b32c6e
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,90 +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
index 3384241f4adaf7ad8e1dbff5be6da70e32a40558,6ec088ac79b79a6f135ee53dcd575ac1e608e0c6..6ec088ac79b79a6f135ee53dcd575ac1e608e0c6
@@@ -1,4 -1,5 +1,5 @@@
  ''''''
  '\''
  '"'
  "'"
@@@ -59,6 -60,7 +60,7 @@@ f"\"{a}\"{'hello' * b}\"{c}\"
  # output
  
  """"""
  "'"
  '"'
  "'"
index 0000000000000000000000000000000000000000,cf8f148f856a85e49489d650bff85a4f1b5c0bda..cf8f148f856a85e49489d650bff85a4f1b5c0bda
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,8 +1,8 @@@
+ [project]
+ name = "test"
+ version = "1.0.0"
+ requires-python = ">=3.7,<3.11"
+ [tool.black]
+ line-length = 79
+ target-version = ["py310"]
index 0000000000000000000000000000000000000000,67623d2279be1bf7291e109c85b8972000de822b..67623d2279be1bf7291e109c85b8972000de822b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,6 +1,6 @@@
+ [project]
+ name = "test"
+ version = "1.0.0"
+ [tool.black]
+ line-length = 79
index 0000000000000000000000000000000000000000,94058bb3b1e67050d7f1e4fca992e34479b6ea33..94058bb3b1e67050d7f1e4fca992e34479b6ea33
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,7 +1,7 @@@
+ [project]
+ name = "test"
+ version = "1.0.0"
+ [tool.black]
+ line-length = 79
+ target-version = ["py310"]
index 0000000000000000000000000000000000000000,1c8cdbb31adff2004e6fb189ef12decb1372140f..1c8cdbb31adff2004e6fb189ef12decb1372140f
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,7 +1,7 @@@
+ [project]
+ name = "test"
+ version = "1.0.0"
+ requires-python = ">=3.7,<3.11"
+ [tool.black]
+ line-length = 79
index 0000000000000000000000000000000000000000,751fd3201dff94e31d430d171b90833571505931..751fd3201dff94e31d430d171b90833571505931
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,32 +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"""
index e12b94cd29e47acf24a6f7b6cabefa7948526525,3f5277b6b034c0b513b9cbd146cdc796680a95c0..3f5277b6b034c0b513b9cbd146cdc796680a95c0
@@@ -14,27 -14,32 +14,32 @@@ Specifying the name of the default beha
  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 +101,7 @@@ def pytest_collection_modifyitems(confi
      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..e5fb9228f19b2e50a89c7c43d58961df8a916b9b
@@@ -7,6 -7,7 +7,7 @@@ line-length = 7
  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..537ca80d4320cdc5dfef82ee0b88f515083139a0
@@@ -6,11 -6,11 +6,11 @@@ import i
  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 +24,7 @@@ from typing import 
      List,
      Optional,
      Sequence,
+     Type,
      TypeVar,
      Union,
  )
@@@ -31,7 -32,6 +32,6 @@@ from unittest.mock import MagicMock, pa
  
  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 +40,7 @@@ import blac
  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 +50,7 @@@ from tests.util import 
      DATA_DIR,
      DEFAULT_MODE,
      DETERMINISTIC_HEADER,
+     PROJECT_ROOT,
      PY36_VERSIONS,
      THIS_DIR,
      BlackBaseTestCase,
      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 +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 +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 +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 +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)
  
      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)
          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)
  
      @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,
          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)
              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:
              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)
          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)
              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]
  
      @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]
          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 = []
              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:
              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:
              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:
  
          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)
          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,
          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)
          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")
          )
          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 = []
  
          )
  
      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"
              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 = []
  
                  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)
      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
      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",
              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",
              # __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",
              # __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",
              # __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",
              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}")
              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,
  
      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:
              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")
          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()
          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)
              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",
              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)])
          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
          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
  
              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."
                  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)
  
                      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:
                  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()
          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")
      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)
              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
          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,
      )
      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,
          report=black.Report(),
          stdin_filename=stdin_filename,
      )
-     assert sorted(list(collected)) == sorted(gs_expected)
+     assert sorted(collected) == sorted(gs_expected)
  
  
  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.
                  None,
                  None,
                  report,
-                 gitignore,
+                 {path: gitignore},
                  verbose=False,
                  quiet=False,
              )
                  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"
          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]
              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")
              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
              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
              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
          )
  
  
- 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..59703036dc07dfd7a41a804a60f4762124b77ed1
@@@ -1,19 -1,33 +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 +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 +93,9 @@@
          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")
  
      @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"}
          )
      @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"}
          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)
      @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("/")
          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..4e863c6c54b4ddb843434c4d4586531a877645d5
@@@ -5,114 -5,15 +5,15 @@@ from unittest.mock import patc
  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]:
          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:
      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..59897190304e7595a82a732273058f33b1407e46
@@@ -1,25 -1,35 +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 +72,19 @@@ def test_trailing_semicolon_noop() -> N
          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 +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",
      (
              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
  
  
      (
          "%%bash\n2+2",
          "%%html --isolated\n2+2",
+         "%%writefile e.txt\n  meh\n meh",
      ),
  )
  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 +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 +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"
  
  
  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"
  
  
  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"
  
  
  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 +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 +423,9 @@@ def test_ipynb_diff_with_no_change() -
      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."
  
  
  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..12c820def390a61ad21df6f88de70af3daa1d7bf
@@@ -1,10 -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 +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
index 0000000000000000000000000000000000000000,784e852e12a53f1b47faea654e9a208347a8c2ea..784e852e12a53f1b47faea654e9a208347a8c2ea
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,51 +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..a31ae0992c2886936fdc0bdb52c9c9213203df7e
@@@ -1,15 -1,25 +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 +39,53 @@@ ff = partial(black.format_file_in_place
  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,
      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 +168,106 @@@ class BlackBaseTestCase(unittest.TestCa
          _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
      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..018cef993c0af0aad7d1a43da7643f76a87116a0
@@@ -1,11 -1,14 +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 =
  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 +83,16 @@@ skip_install = Tru
  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