X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/25d24a10a4f4cdf81b7335615c74f285127b1a04..1d3fb871bec82d1263940ba1d32095d660fea2ab:/black.py

diff --git a/black.py b/black.py
index f76f0ff..7b905ee 100644
--- a/black.py
+++ b/black.py
@@ -1,20 +1,25 @@
+import ast
 import asyncio
-from asyncio.base_events import BaseEventLoop
+from abc import ABC, abstractmethod
+from collections import defaultdict
 from concurrent.futures import Executor, ProcessPoolExecutor
+from contextlib import contextmanager
 from datetime import datetime
-from enum import Enum, Flag
+from enum import Enum
 from functools import lru_cache, partial, wraps
 import io
-import keyword
+import itertools
 import logging
-from multiprocessing import Manager
+from multiprocessing import Manager, freeze_support
 import os
 from pathlib import Path
 import pickle
-import re
+import regex as re
 import signal
 import sys
+import tempfile
 import tokenize
+import traceback
 from typing import (
     Any,
     Callable,
@@ -30,31 +35,37 @@ from typing import (
     Sequence,
     Set,
     Tuple,
+    Type,
     TypeVar,
     Union,
     cast,
 )
+from typing_extensions import Final
+from mypy_extensions import mypyc_attr
 
 from appdirs import user_cache_dir
-from attr import dataclass, Factory
+from dataclasses import dataclass, field, replace
 import click
 import toml
+from typed_ast import ast3, ast27
+from pathspec import PathSpec
 
 # lib2to3 fork
 from blib2to3.pytree import Node, Leaf, type_repr
 from blib2to3 import pygram, pytree
 from blib2to3.pgen2 import driver, token
+from blib2to3.pgen2.grammar import Grammar
 from blib2to3.pgen2.parse import ParseError
 
+from _black_version import version as __version__
 
-__version__ = "18.6b4"
 DEFAULT_LINE_LENGTH = 88
-DEFAULT_EXCLUDES = (
-    r"/(\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist)/"
-)
+DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/"  # noqa: B950
 DEFAULT_INCLUDES = r"\.pyi?$"
 CACHE_DIR = Path(user_cache_dir("black", version=__version__))
 
+STRING_PREFIX_CHARS: Final = "furbFURB"  # All possible string prefix characters.
+
 
 # types
 FileContent = str
@@ -62,11 +73,13 @@ Encoding = str
 NewLine = str
 Depth = int
 NodeType = int
+ParserState = int
 LeafID = int
+StringID = int
 Priority = int
 Index = int
 LN = Union[Leaf, Node]
-SplitFunc = Callable[["Line", bool], Iterator["Line"]]
+Transformer = Callable[["Line", Collection["Feature"]], Iterator["Line"]]
 Timestamp = float
 FileSize = int
 CacheInfo = Tuple[Timestamp, FileSize]
@@ -82,7 +95,11 @@ class NothingChanged(UserWarning):
     """Raised when reformatted code is the same as source."""
 
 
-class CannotSplit(Exception):
+class CannotTransform(Exception):
+    """Base class for errors raised by Transformers."""
+
+
+class CannotSplit(CannotTransform):
     """A readable split that fits the allotted line length is impossible."""
 
 
@@ -90,6 +107,34 @@ class InvalidInput(ValueError):
     """Raised when input source code fails all parse attempts."""
 
 
+T = TypeVar("T")
+E = TypeVar("E", bound=Exception)
+
+
+class Ok(Generic[T]):
+    def __init__(self, value: T) -> None:
+        self._value = value
+
+    def ok(self) -> T:
+        return self._value
+
+
+class Err(Generic[E]):
+    def __init__(self, e: E) -> None:
+        self._e = e
+
+    def err(self) -> E:
+        return self._e
+
+
+# The 'Result' return type is used to implement an error-handling model heavily
+# influenced by that used by the Rust programming language
+# (see https://doc.rust-lang.org/book/ch09-00-error-handling.html).
+Result = Union[Ok[T], Err[E]]
+TResult = Result[T, CannotTransform]  # (T)ransform Result
+TMatchResult = TResult[Index]
+
+
 class WriteBack(Enum):
     NO = 0
     YES = 1
@@ -110,69 +155,175 @@ class Changed(Enum):
     YES = 2
 
 
-class FileMode(Flag):
-    AUTO_DETECT = 0
-    PYTHON36 = 1
-    PYI = 2
-    NO_STRING_NORMALIZATION = 4
-    NO_NUMERIC_UNDERSCORE_NORMALIZATION = 8
+class TargetVersion(Enum):
+    PY27 = 2
+    PY33 = 3
+    PY34 = 4
+    PY35 = 5
+    PY36 = 6
+    PY37 = 7
+    PY38 = 8
+
+    def is_python2(self) -> bool:
+        return self is TargetVersion.PY27
+
+
+PY36_VERSIONS = {TargetVersion.PY36, TargetVersion.PY37, TargetVersion.PY38}
+
+
+class Feature(Enum):
+    # All string literals are unicode
+    UNICODE_LITERALS = 1
+    F_STRINGS = 2
+    NUMERIC_UNDERSCORES = 3
+    TRAILING_COMMA_IN_CALL = 4
+    TRAILING_COMMA_IN_DEF = 5
+    # The following two feature-flags are mutually exclusive, and exactly one should be
+    # set for every version of python.
+    ASYNC_IDENTIFIERS = 6
+    ASYNC_KEYWORDS = 7
+    ASSIGNMENT_EXPRESSIONS = 8
+    POS_ONLY_ARGUMENTS = 9
+
+
+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.PY36: {
+        Feature.UNICODE_LITERALS,
+        Feature.F_STRINGS,
+        Feature.NUMERIC_UNDERSCORES,
+        Feature.TRAILING_COMMA_IN_CALL,
+        Feature.TRAILING_COMMA_IN_DEF,
+        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,
+    },
+    TargetVersion.PY38: {
+        Feature.UNICODE_LITERALS,
+        Feature.F_STRINGS,
+        Feature.NUMERIC_UNDERSCORES,
+        Feature.TRAILING_COMMA_IN_CALL,
+        Feature.TRAILING_COMMA_IN_DEF,
+        Feature.ASYNC_KEYWORDS,
+        Feature.ASSIGNMENT_EXPRESSIONS,
+        Feature.POS_ONLY_ARGUMENTS,
+    },
+}
+
+
+@dataclass
+class Mode:
+    target_versions: Set[TargetVersion] = field(default_factory=set)
+    line_length: int = DEFAULT_LINE_LENGTH
+    string_normalization: bool = True
+    is_pyi: bool = False
+
+    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)
+            )
+        else:
+            version_str = "-"
+        parts = [
+            version_str,
+            str(self.line_length),
+            str(int(self.string_normalization)),
+            str(int(self.is_pyi)),
+        ]
+        return ".".join(parts)
+
+
+# Legacy name, left for integrations.
+FileMode = Mode
+
+
+def supports_feature(target_versions: Set[TargetVersion], feature: Feature) -> bool:
+    return all(feature in VERSION_TO_FEATURES[version] for version in target_versions)
+
+
+def find_pyproject_toml(path_search_start: str) -> Optional[str]:
+    """Find the absolute filepath to a pyproject.toml if it exists"""
+    path_project_root = find_project_root(path_search_start)
+    path_pyproject_toml = path_project_root / "pyproject.toml"
+    return str(path_pyproject_toml) if path_pyproject_toml.is_file() else None
 
-    @classmethod
-    def from_configuration(
-        cls,
-        *,
-        py36: bool,
-        pyi: bool,
-        skip_string_normalization: bool,
-        skip_numeric_underscore_normalization: bool,
-    ) -> "FileMode":
-        mode = cls.AUTO_DETECT
-        if py36:
-            mode |= cls.PYTHON36
-        if pyi:
-            mode |= cls.PYI
-        if skip_string_normalization:
-            mode |= cls.NO_STRING_NORMALIZATION
-        if skip_numeric_underscore_normalization:
-            mode |= cls.NO_NUMERIC_UNDERSCORE_NORMALIZATION
-        return mode
+
+def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
+    """Parse a pyproject toml file, pulling out relevant parts for Black
+
+    If parsing fails, will raise a toml.TomlDecodeError
+    """
+    pyproject_toml = toml.load(path_config)
+    config = pyproject_toml.get("tool", {}).get("black", {})
+    return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
 
 
 def read_pyproject_toml(
-    ctx: click.Context, param: click.Parameter, value: Union[str, int, bool, None]
+    ctx: click.Context, param: click.Parameter, value: Optional[str]
 ) -> Optional[str]:
     """Inject Black configuration from "pyproject.toml" into defaults in `ctx`.
 
     Returns the path to a successfully found and read configuration file, None
     otherwise.
     """
-    assert not isinstance(value, (int, bool)), "Invalid parameter type passed"
     if not value:
-        root = find_project_root(ctx.params.get("src", ()))
-        path = root / "pyproject.toml"
-        if path.is_file():
-            value = str(path)
-        else:
+        value = find_pyproject_toml(ctx.params.get("src", ()))
+        if value is None:
             return None
 
     try:
-        pyproject_toml = toml.load(value)
-        config = pyproject_toml.get("tool", {}).get("black", {})
+        config = parse_pyproject_toml(value)
     except (toml.TomlDecodeError, OSError) as e:
-        raise click.BadOptionUsage(f"Error reading configuration file: {e}", ctx)
+        raise click.FileError(
+            filename=value, hint=f"Error reading configuration file: {e}"
+        )
 
     if not config:
         return None
 
-    if ctx.default_map is None:
-        ctx.default_map = {}
-    ctx.default_map.update(  # type: ignore  # bad types in .pyi
-        {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
-    )
+    target_version = config.get("target_version")
+    if target_version is not None and not isinstance(target_version, list):
+        raise click.BadOptionUsage(
+            "target-version", f"Config key target-version must be a list"
+        )
+
+    default_map: Dict[str, Any] = {}
+    if ctx.default_map:
+        default_map.update(ctx.default_map)
+    default_map.update(config)
+
+    ctx.default_map = default_map
     return value
 
 
+def target_version_option_callback(
+    c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...]
+) -> List[TargetVersion]:
+    """Compute the target versions from a --target-version flag.
+
+    This is its own function because mypy couldn't infer the type correctly
+    when it was a lambda, causing mypyc trouble.
+    """
+    return [TargetVersion[val.upper()] for val in v]
+
+
 @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
+@click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
 @click.option(
     "-l",
     "--line-length",
@@ -181,21 +332,32 @@ def read_pyproject_toml(
     help="How many characters per line to allow.",
     show_default=True,
 )
+@click.option(
+    "-t",
+    "--target-version",
+    type=click.Choice([v.name.lower() for v in TargetVersion]),
+    callback=target_version_option_callback,
+    multiple=True,
+    help=(
+        "Python versions that should be supported by Black's output. [default: per-file"
+        " auto-detection]"
+    ),
+)
 @click.option(
     "--py36",
     is_flag=True,
     help=(
-        "Allow using Python 3.6-only syntax on all input files.  This will put "
-        "trailing commas in function signatures and calls also after *args and "
-        "**kwargs.  [default: per-file auto-detection]"
+        "Allow using Python 3.6-only syntax on all input files.  This will put trailing"
+        " commas in function signatures and calls also after *args and **kwargs."
+        " Deprecated; use --target-version instead. [default: per-file auto-detection]"
     ),
 )
 @click.option(
     "--pyi",
     is_flag=True,
     help=(
-        "Format all input files like typing stubs regardless of file extension "
-        "(useful when piping source on standard input)."
+        "Format all input files like typing stubs regardless of file extension (useful"
+        " when piping source on standard input)."
     ),
 )
 @click.option(
@@ -204,19 +366,13 @@ def read_pyproject_toml(
     is_flag=True,
     help="Don't normalize string quotes or prefixes.",
 )
-@click.option(
-    "-N",
-    "--skip-numeric-underscore-normalization",
-    is_flag=True,
-    help="Don't normalize underscores in numeric literals.",
-)
 @click.option(
     "--check",
     is_flag=True,
     help=(
-        "Don't write the files back, just return the status.  Return code 0 "
-        "means nothing would change.  Return code 1 means some files would be "
-        "reformatted.  Return code 123 means there was an internal error."
+        "Don't write the files back, just return the status.  Return code 0 means"
+        " nothing would change.  Return code 1 means some files would be reformatted."
+        " Return code 123 means there was an internal error."
     ),
 )
 @click.option(
@@ -234,11 +390,10 @@ def read_pyproject_toml(
     type=str,
     default=DEFAULT_INCLUDES,
     help=(
-        "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."
+        "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."
     ),
     show_default=True,
 )
@@ -247,10 +402,10 @@ def read_pyproject_toml(
     type=str,
     default=DEFAULT_EXCLUDES,
     help=(
-        "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."
+        "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."
     ),
     show_default=True,
 )
@@ -259,8 +414,8 @@ def read_pyproject_toml(
     "--quiet",
     is_flag=True,
     help=(
-        "Don't emit non-error messages to stderr. Errors are still emitted, "
-        "silence those with 2>/dev/null."
+        "Don't emit non-error messages to stderr. Errors are still emitted; silence"
+        " those with 2>/dev/null."
     ),
 )
 @click.option(
@@ -268,8 +423,8 @@ def read_pyproject_toml(
     "--verbose",
     is_flag=True,
     help=(
-        "Also emit messages to stderr about files that were not changed or were "
-        "ignored due to --exclude=."
+        "Also emit messages to stderr about files that were not changed or were ignored"
+        " due to --exclude=."
     ),
 )
 @click.version_option(version=__version__)
@@ -284,7 +439,12 @@ def read_pyproject_toml(
 @click.option(
     "--config",
     type=click.Path(
-        exists=False, file_okay=True, dir_okay=False, readable=True, allow_dash=False
+        exists=True,
+        file_okay=True,
+        dir_okay=False,
+        readable=True,
+        allow_dash=False,
+        path_type=str,
     ),
     is_eager=True,
     callback=read_pyproject_toml,
@@ -293,31 +453,50 @@ def read_pyproject_toml(
 @click.pass_context
 def main(
     ctx: click.Context,
+    code: Optional[str],
     line_length: int,
+    target_version: List[TargetVersion],
     check: bool,
     diff: bool,
     fast: bool,
     pyi: bool,
     py36: bool,
     skip_string_normalization: bool,
-    skip_numeric_underscore_normalization: bool,
     quiet: bool,
     verbose: bool,
     include: str,
     exclude: str,
-    src: Tuple[str],
+    src: Tuple[str, ...],
     config: Optional[str],
 ) -> None:
     """The uncompromising code formatter."""
     write_back = WriteBack.from_configuration(check=check, diff=diff)
-    mode = FileMode.from_configuration(
-        py36=py36,
-        pyi=pyi,
-        skip_string_normalization=skip_string_normalization,
-        skip_numeric_underscore_normalization=skip_numeric_underscore_normalization,
+    if target_version:
+        if py36:
+            err("Cannot use both --target-version and --py36")
+            ctx.exit(2)
+        else:
+            versions = set(target_version)
+    elif py36:
+        err(
+            "--py36 is deprecated and will be removed in a future version. Use"
+            " --target-version py36 instead."
+        )
+        versions = PY36_VERSIONS
+    else:
+        # We'll autodetect later.
+        versions = set()
+    mode = Mode(
+        target_versions=versions,
+        line_length=line_length,
+        is_pyi=pyi,
+        string_normalization=not skip_string_normalization,
     )
     if config and verbose:
         out(f"Using configuration from {config}.", bold=False, fg="blue")
+    if code is not None:
+        print(format_str(code, mode=mode))
+        ctx.exit(0)
     try:
         include_regex = re_compile_maybe_verbose(include)
     except re.error:
@@ -328,14 +507,17 @@ def main(
     except re.error:
         err(f"Invalid regular expression for exclude given: {exclude!r}")
         ctx.exit(2)
-    report = Report(check=check, quiet=quiet, verbose=verbose)
+    report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
     root = find_project_root(src)
     sources: Set[Path] = set()
+    path_empty(src, quiet, verbose, ctx)
     for s in src:
         p = Path(s)
         if p.is_dir():
             sources.update(
-                gen_python_files_in_dir(p, root, include_regex, exclude_regex, report)
+                gen_python_files_in_dir(
+                    p, root, include_regex, exclude_regex, report, get_gitignore(root)
+                )
             )
         elif p.is_file() or s == "-":
             # if a file was explicitly given, we don't care about its extension
@@ -344,108 +526,119 @@ def main(
             err(f"invalid path: {s}")
     if len(sources) == 0:
         if verbose or not quiet:
-            out("No paths given. Nothing to do 😴")
+            out("No Python files are present to be formatted. Nothing to do 😴")
         ctx.exit(0)
 
     if len(sources) == 1:
         reformat_one(
             src=sources.pop(),
-            line_length=line_length,
             fast=fast,
             write_back=write_back,
             mode=mode,
             report=report,
         )
     else:
-        loop = asyncio.get_event_loop()
-        executor = ProcessPoolExecutor(max_workers=os.cpu_count())
-        try:
-            loop.run_until_complete(
-                schedule_formatting(
-                    sources=sources,
-                    line_length=line_length,
-                    fast=fast,
-                    write_back=write_back,
-                    mode=mode,
-                    report=report,
-                    loop=loop,
-                    executor=executor,
-                )
-            )
-        finally:
-            shutdown(loop)
+        reformat_many(
+            sources=sources, fast=fast, write_back=write_back, mode=mode, report=report
+        )
+
     if verbose or not quiet:
-        bang = "💥 💔 💥" if report.return_code else "✨ 🍰 ✨"
-        out(f"All done! {bang}")
+        out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
         click.secho(str(report), err=True)
     ctx.exit(report.return_code)
 
 
+def path_empty(
+    src: Tuple[str, ...], quiet: bool, verbose: bool, ctx: click.Context
+) -> None:
+    """
+    Exit if there is no `src` provided for formatting
+    """
+    if not src:
+        if verbose or not quiet:
+            out("No Path provided. Nothing to do 😴")
+            ctx.exit(0)
+
+
 def reformat_one(
-    src: Path,
-    line_length: int,
-    fast: bool,
-    write_back: WriteBack,
-    mode: FileMode,
-    report: "Report",
+    src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
 ) -> None:
     """Reformat a single file under `src` without spawning child processes.
 
-    If `quiet` is True, non-error messages are not output. `line_length`,
-    `write_back`, `fast` and `pyi` options are passed to
+    `fast`, `write_back`, and `mode` options are passed to
     :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
     """
     try:
         changed = Changed.NO
         if not src.is_file() and str(src) == "-":
-            if format_stdin_to_stdout(
-                line_length=line_length, fast=fast, write_back=write_back, mode=mode
-            ):
+            if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode):
                 changed = Changed.YES
         else:
             cache: Cache = {}
             if write_back != WriteBack.DIFF:
-                cache = read_cache(line_length, mode)
+                cache = read_cache(mode)
                 res_src = src.resolve()
                 if res_src in cache and cache[res_src] == get_cache_info(res_src):
                     changed = Changed.CACHED
             if changed is not Changed.CACHED and format_file_in_place(
-                src,
-                line_length=line_length,
-                fast=fast,
-                write_back=write_back,
-                mode=mode,
+                src, fast=fast, write_back=write_back, mode=mode
             ):
                 changed = Changed.YES
             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], line_length, mode)
+                write_cache(cache, [src], mode)
         report.done(src, changed)
     except Exception as exc:
         report.failed(src, str(exc))
 
 
+def reformat_many(
+    sources: Set[Path], fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
+) -> None:
+    """Reformat multiple files using a ProcessPoolExecutor."""
+    loop = asyncio.get_event_loop()
+    worker_count = os.cpu_count()
+    if sys.platform == "win32":
+        # Work around https://bugs.python.org/issue26903
+        worker_count = min(worker_count, 61)
+    executor = ProcessPoolExecutor(max_workers=worker_count)
+    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)
+        executor.shutdown()
+
+
 async def schedule_formatting(
     sources: Set[Path],
-    line_length: int,
     fast: bool,
     write_back: WriteBack,
-    mode: FileMode,
+    mode: Mode,
     report: "Report",
-    loop: BaseEventLoop,
+    loop: asyncio.AbstractEventLoop,
     executor: Executor,
 ) -> None:
     """Run formatting of `sources` in parallel using the provided `executor`.
 
     (Use ProcessPoolExecutors for actual parallelism.)
 
-    `line_length`, `write_back`, `fast`, and `pyi` options are passed to
+    `write_back`, `fast`, and `mode` options are passed to
     :func:`format_file_in_place`.
     """
     cache: Cache = {}
     if write_back != WriteBack.DIFF:
-        cache = read_cache(line_length, mode)
+        cache = read_cache(mode)
         sources, cached = filter_cached(cache, sources)
         for src in sorted(cached):
             report.done(src, Changed.CACHED)
@@ -461,19 +654,14 @@ async def schedule_formatting(
         manager = Manager()
         lock = manager.Lock()
     tasks = {
-        loop.run_in_executor(
-            executor,
-            format_file_in_place,
-            src,
-            line_length,
-            fast,
-            write_back,
-            mode,
-            lock,
+        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: Iterable[asyncio.Task] = tasks.keys()
+    pending: Iterable["asyncio.Future[bool]"] = tasks.keys()
     try:
         loop.add_signal_handler(signal.SIGINT, cancel, pending)
         loop.add_signal_handler(signal.SIGTERM, cancel, pending)
@@ -500,47 +688,43 @@ async def schedule_formatting(
     if cancelled:
         await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
     if sources_to_cache:
-        write_cache(cache, sources_to_cache, line_length, mode)
+        write_cache(cache, sources_to_cache, mode)
 
 
 def format_file_in_place(
     src: Path,
-    line_length: int,
     fast: bool,
+    mode: Mode,
     write_back: WriteBack = WriteBack.NO,
-    mode: FileMode = FileMode.AUTO_DETECT,
     lock: Any = None,  # multiprocessing.Manager().Lock() is some crazy proxy
 ) -> bool:
     """Format file under `src` path. Return True if changed.
 
     If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted
     code to the file.
-    `line_length` and `fast` options are passed to :func:`format_file_contents`.
+    `mode` and `fast` options are passed to :func:`format_file_contents`.
     """
     if src.suffix == ".pyi":
-        mode |= FileMode.PYI
+        mode = replace(mode, is_pyi=True)
 
     then = datetime.utcfromtimestamp(src.stat().st_mtime)
     with open(src, "rb") as buf:
         src_contents, encoding, newline = decode_bytes(buf.read())
     try:
-        dst_contents = format_file_contents(
-            src_contents, line_length=line_length, fast=fast, mode=mode
-        )
+        dst_contents = format_file_contents(src_contents, fast=fast, mode=mode)
     except NothingChanged:
         return False
 
-    if write_back == write_back.YES:
+    if write_back == WriteBack.YES:
         with open(src, "w", encoding=encoding, newline=newline) as f:
             f.write(dst_contents)
-    elif write_back == write_back.DIFF:
+    elif write_back == WriteBack.DIFF:
         now = datetime.utcnow()
         src_name = f"{src}\t{then} +0000"
         dst_name = f"{src}\t{now} +0000"
         diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
-        if lock:
-            lock.acquire()
-        try:
+
+        with lock or nullcontext():
             f = io.TextIOWrapper(
                 sys.stdout.buffer,
                 encoding=encoding,
@@ -549,30 +733,24 @@ def format_file_in_place(
             )
             f.write(diff_contents)
             f.detach()
-        finally:
-            if lock:
-                lock.release()
+
     return True
 
 
 def format_stdin_to_stdout(
-    line_length: int,
-    fast: bool,
-    write_back: WriteBack = WriteBack.NO,
-    mode: FileMode = FileMode.AUTO_DETECT,
+    fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode
 ) -> bool:
     """Format file on stdin. Return True if changed.
 
     If `write_back` is YES, write reformatted code back to stdout. If it is DIFF,
-    write a diff to stdout.
-    `line_length`, `fast`, `is_pyi`, and `force_py36` arguments are passed to
+    write a diff to stdout. The `mode` argument is passed to
     :func:`format_file_contents`.
     """
     then = datetime.utcnow()
     src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
     dst = src
     try:
-        dst = format_file_contents(src, line_length=line_length, fast=fast, mode=mode)
+        dst = format_file_contents(src, fast=fast, mode=mode)
         return True
 
     except NothingChanged:
@@ -592,65 +770,89 @@ def format_stdin_to_stdout(
         f.detach()
 
 
-def format_file_contents(
-    src_contents: str,
-    *,
-    line_length: int,
-    fast: bool,
-    mode: FileMode = FileMode.AUTO_DETECT,
-) -> FileContent:
+def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
     """Reformat contents a file and return new contents.
 
     If `fast` is False, additionally confirm that the reformatted code is
     valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
-    `line_length` is passed to :func:`format_str`.
+    `mode` is passed to :func:`format_str`.
     """
     if src_contents.strip() == "":
         raise NothingChanged
 
-    dst_contents = format_str(src_contents, line_length=line_length, mode=mode)
+    dst_contents = format_str(src_contents, mode=mode)
     if src_contents == dst_contents:
         raise NothingChanged
 
     if not fast:
         assert_equivalent(src_contents, dst_contents)
-        assert_stable(src_contents, dst_contents, line_length=line_length, mode=mode)
+        assert_stable(src_contents, dst_contents, mode=mode)
     return dst_contents
 
 
-def format_str(
-    src_contents: str, line_length: int, *, mode: FileMode = FileMode.AUTO_DETECT
-) -> FileContent:
+def format_str(src_contents: str, *, mode: Mode) -> FileContent:
     """Reformat a string and return new contents.
 
-    `line_length` determines how many characters per line are allowed.
+    `mode` determines formatting options, such as how many characters per line are
+    allowed.  Example:
+
+    >>> import black
+    >>> print(black.format_str("def f(arg:str='')->None:...", mode=Mode()))
+    def f(arg: str = "") -> None:
+        ...
+
+    A more complex example:
+    >>> print(
+    ...   black.format_str(
+    ...     "def f(arg:str='')->None: hey",
+    ...     mode=black.Mode(
+    ...       target_versions={black.TargetVersion.PY36},
+    ...       line_length=10,
+    ...       string_normalization=False,
+    ...       is_pyi=False,
+    ...     ),
+    ...   ),
+    ... )
+    def f(
+        arg: str = '',
+    ) -> None:
+        hey
+
     """
-    src_node = lib2to3_parse(src_contents)
-    dst_contents = ""
+    src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
+    dst_contents = []
     future_imports = get_future_imports(src_node)
-    is_pyi = bool(mode & FileMode.PYI)
-    py36 = bool(mode & FileMode.PYTHON36) or is_python36(src_node)
-    normalize_strings = not bool(mode & FileMode.NO_STRING_NORMALIZATION)
+    if mode.target_versions:
+        versions = mode.target_versions
+    else:
+        versions = detect_target_versions(src_node)
     normalize_fmt_off(src_node)
     lines = LineGenerator(
-        remove_u_prefix=py36 or "unicode_literals" in future_imports,
-        is_pyi=is_pyi,
-        normalize_strings=normalize_strings,
-        allow_underscores=py36
-        and not bool(mode & FileMode.NO_NUMERIC_UNDERSCORE_NORMALIZATION),
+        remove_u_prefix="unicode_literals" in future_imports
+        or supports_feature(versions, Feature.UNICODE_LITERALS),
+        is_pyi=mode.is_pyi,
+        normalize_strings=mode.string_normalization,
     )
-    elt = EmptyLineTracker(is_pyi=is_pyi)
+    elt = EmptyLineTracker(is_pyi=mode.is_pyi)
     empty_line = Line()
     after = 0
+    split_line_features = {
+        feature
+        for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
+        if supports_feature(versions, feature)
+    }
     for current_line in lines.visit(src_node):
-        for _ in range(after):
-            dst_contents += str(empty_line)
+        dst_contents.append(str(empty_line) * after)
         before, after = elt.maybe_empty_lines(current_line)
-        for _ in range(before):
-            dst_contents += str(empty_line)
-        for line in split_line(current_line, line_length=line_length, py36=py36):
-            dst_contents += str(line)
-    return dst_contents
+        dst_contents.append(str(empty_line) * before)
+        for line in transform_line(
+            current_line,
+            line_length=mode.line_length,
+            normalize_strings=mode.string_normalization,
+            features=split_line_features,
+        ):
+            dst_contents.append(str(line))
+    return "".join(dst_contents)
 
 
 def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
@@ -670,19 +872,51 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
         return tiow.read(), encoding, newline
 
 
-GRAMMARS = [
-    pygram.python_grammar_no_print_statement_no_exec_statement,
-    pygram.python_grammar_no_print_statement,
-    pygram.python_grammar,
-]
+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,
+        ]
+
+    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-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_KEYWORDS):
+        # Python 3.0-3.6
+        grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement)
+    # 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) -> Node:
+def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node:
     """Given a string with source, return the lib2to3 Node."""
-    grammar = pygram.python_grammar_no_print_statement
     if src_txt[-1:] != "\n":
         src_txt += "\n"
-    for grammar in GRAMMARS:
+
+    for grammar in get_grammars(set(target_versions)):
         drv = driver.Driver(grammar, pytree.convert)
         try:
             result = drv.parse_string(src_txt, True)
@@ -710,9 +944,6 @@ def lib2to3_unparse(node: Node) -> str:
     return code
 
 
-T = TypeVar("T")
-
-
 class Visitor(Generic[T]):
     """Basic lib2to3 visitor that yields things of type `T` on `visit()`."""
 
@@ -729,8 +960,16 @@ class Visitor(Generic[T]):
         if node.type < 256:
             name = token.tok_name[node.type]
         else:
-            name = type_repr(node.type)
-        yield from getattr(self, f"visit_{name}", self.visit_default)(node)
+            name = str(type_repr(node.type))
+        # We explicitly branch on whether a visitor exists (instead of
+        # using self.visit_default as the default arg to getattr) in order
+        # to save needing to create a bound method object and so mypyc can
+        # generate a native call to visit_default.
+        visitf = getattr(self, f"visit_{name}", None)
+        if visitf:
+            yield from visitf(node)
+        else:
+            yield from self.visit_default(node)
 
     def visit_default(self, node: LN) -> Iterator[T]:
         """Default `visit_*()` implementation. Recurses to children of `node`."""
@@ -775,10 +1014,8 @@ class DebugVisitor(Visitor[T]):
         list(v.visit(code))
 
 
-KEYWORDS = set(keyword.kwlist)
-WHITESPACE = {token.DEDENT, token.INDENT, token.NEWLINE}
-FLOW_CONTROL = {"return", "raise", "break", "continue"}
-STATEMENT = {
+WHITESPACE: Final = {token.DEDENT, token.INDENT, token.NEWLINE}
+STATEMENT: Final = {
     syms.if_stmt,
     syms.while_stmt,
     syms.for_stmt,
@@ -788,10 +1025,10 @@ STATEMENT = {
     syms.funcdef,
     syms.classdef,
 }
-STANDALONE_COMMENT = 153
+STANDALONE_COMMENT: Final = 153
 token.tok_name[STANDALONE_COMMENT] = "STANDALONE_COMMENT"
-LOGIC_OPERATORS = {"and", "or"}
-COMPARATORS = {
+LOGIC_OPERATORS: Final = {"and", "or"}
+COMPARATORS: Final = {
     token.LESS,
     token.GREATER,
     token.EQEQUAL,
@@ -799,7 +1036,7 @@ COMPARATORS = {
     token.LESSEQUAL,
     token.GREATEREQUAL,
 }
-MATH_OPERATORS = {
+MATH_OPERATORS: Final = {
     token.VBAR,
     token.CIRCUMFLEX,
     token.AMPER,
@@ -815,22 +1052,23 @@ MATH_OPERATORS = {
     token.TILDE,
     token.DOUBLESTAR,
 }
-STARS = {token.STAR, token.DOUBLESTAR}
-VARARGS_PARENTS = {
+STARS: Final = {token.STAR, token.DOUBLESTAR}
+VARARGS_SPECIALS: Final = STARS | {token.SLASH}
+VARARGS_PARENTS: Final = {
     syms.arglist,
     syms.argument,  # double star in arglist
     syms.trailer,  # single argument to call
     syms.typedargslist,
     syms.varargslist,  # lambdas
 }
-UNPACKING_PARENTS = {
+UNPACKING_PARENTS: Final = {
     syms.atom,  # single element of a list or set literal
     syms.dictsetmaker,
     syms.listmaker,
     syms.testlist_gexp,
     syms.testlist_star_expr,
 }
-TEST_DESCENDANTS = {
+TEST_DESCENDANTS: Final = {
     syms.test,
     syms.lambdef,
     syms.or_test,
@@ -847,7 +1085,7 @@ TEST_DESCENDANTS = {
     syms.term,
     syms.power,
 }
-ASSIGNMENTS = {
+ASSIGNMENTS: Final = {
     "=",
     "+=",
     "-=",
@@ -863,13 +1101,13 @@ ASSIGNMENTS = {
     "**=",
     "//=",
 }
-COMPREHENSION_PRIORITY = 20
-COMMA_PRIORITY = 18
-TERNARY_PRIORITY = 16
-LOGIC_PRIORITY = 14
-STRING_PRIORITY = 12
-COMPARATOR_PRIORITY = 10
-MATH_PRIORITIES = {
+COMPREHENSION_PRIORITY: Final = 20
+COMMA_PRIORITY: Final = 18
+TERNARY_PRIORITY: Final = 16
+LOGIC_PRIORITY: Final = 14
+STRING_PRIORITY: Final = 12
+COMPARATOR_PRIORITY: Final = 10
+MATH_PRIORITIES: Final = {
     token.VBAR: 9,
     token.CIRCUMFLEX: 8,
     token.AMPER: 7,
@@ -885,7 +1123,7 @@ MATH_PRIORITIES = {
     token.TILDE: 3,
     token.DOUBLESTAR: 2,
 }
-DOT_PRIORITY = 1
+DOT_PRIORITY: Final = 1
 
 
 @dataclass
@@ -893,11 +1131,11 @@ class BracketTracker:
     """Keeps track of brackets on a line."""
 
     depth: int = 0
-    bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = Factory(dict)
-    delimiters: Dict[LeafID, Priority] = Factory(dict)
+    bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = field(default_factory=dict)
+    delimiters: Dict[LeafID, Priority] = field(default_factory=dict)
     previous: Optional[Leaf] = None
-    _for_loop_depths: List[int] = Factory(list)
-    _lambda_argument_depths: List[int] = Factory(list)
+    _for_loop_depths: List[int] = field(default_factory=list)
+    _lambda_argument_depths: List[int] = field(default_factory=list)
 
     def mark(self, leaf: Leaf) -> None:
         """Mark `leaf` with bracket-related metadata. Keep track of delimiters.
@@ -943,7 +1181,7 @@ class BracketTracker:
         """Return True if there is an yet unmatched open bracket on the line."""
         return bool(self.bracket_match)
 
-    def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> int:
+    def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority:
         """Return the highest priority of a delimiter found on the line.
 
         Values are consistent with what `is_split_*_delimiter()` return.
@@ -951,7 +1189,7 @@ class BracketTracker:
         """
         return max(v for k, v in self.delimiters.items() if k not in exclude)
 
-    def delimiter_count_with_priority(self, priority: int = 0) -> int:
+    def delimiter_count_with_priority(self, priority: Priority = 0) -> int:
         """Return the number of delimiters with the given `priority`.
 
         If no `priority` is passed, defaults to max priority on the line.
@@ -1025,9 +1263,10 @@ class Line:
     """Holds leaves and comments. Can be printed with `str(line)`."""
 
     depth: int = 0
-    leaves: List[Leaf] = Factory(list)
-    comments: List[Tuple[Index, Leaf]] = Factory(list)
-    bracket_tracker: BracketTracker = Factory(BracketTracker)
+    leaves: List[Leaf] = field(default_factory=list)
+    # keys ordered like `leaves`
+    comments: Dict[LeafID, List[Leaf]] = field(default_factory=dict)
+    bracket_tracker: BracketTracker = field(default_factory=BracketTracker)
     inside_brackets: bool = False
     should_explode: bool = False
 
@@ -1107,6 +1346,69 @@ class Line:
             Leaf(token.DOT, ".") for _ in range(3)
         ]
 
+    @property
+    def is_collection_with_optional_trailing_comma(self) -> bool:
+        """Is this line a collection literal with a trailing comma that's optional?
+
+        Note that the trailing comma in a 1-tuple is not optional.
+        """
+        if not self.leaves or len(self.leaves) < 4:
+            return False
+
+        # Look for and address a trailing colon.
+        if self.leaves[-1].type == token.COLON:
+            closer = self.leaves[-2]
+            close_index = -2
+        else:
+            closer = self.leaves[-1]
+            close_index = -1
+        if closer.type not in CLOSING_BRACKETS or self.inside_brackets:
+            return False
+
+        if closer.type == token.RPAR:
+            # Tuples require an extra check, because if there's only
+            # one element in the tuple removing the comma unmakes the
+            # tuple.
+            #
+            # We also check for parens before looking for the trailing
+            # comma because in some cases (eg assigning a dict
+            # literal) the literal gets wrapped in temporary parens
+            # during parsing. This case is covered by the
+            # collections.py test data.
+            opener = closer.opening_bracket
+            for _open_index, leaf in enumerate(self.leaves):
+                if leaf is opener:
+                    break
+
+            else:
+                # Couldn't find the matching opening paren, play it safe.
+                return False
+
+            commas = 0
+            comma_depth = self.leaves[close_index - 1].bracket_depth
+            for leaf in self.leaves[_open_index + 1 : close_index]:
+                if leaf.bracket_depth == comma_depth and leaf.type == token.COMMA:
+                    commas += 1
+            if commas > 1:
+                # We haven't looked yet for the trailing comma because
+                # we might also have caught noop parens.
+                return self.leaves[close_index - 1].type == token.COMMA
+
+            elif commas == 1:
+                return False  # it's either a one-tuple or didn't have a trailing comma
+
+            if self.leaves[close_index - 1].type in CLOSING_BRACKETS:
+                close_index -= 1
+                closer = self.leaves[close_index]
+                if closer.type == token.RPAR:
+                    # TODO: this is a gut feeling. Will we ever see this?
+                    return False
+
+        if self.leaves[close_index - 1].type != token.COMMA:
+            return False
+
+        return True
+
     @property
     def is_def(self) -> bool:
         """Is this a function definition? (Also returns True for async defs.)"""
@@ -1154,72 +1456,117 @@ class Line:
     def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool:
         """If so, needs to be split before emitting."""
         for leaf in self.leaves:
-            if leaf.type == STANDALONE_COMMENT:
-                if leaf.bracket_depth <= depth_limit:
-                    return True
+            if leaf.type == STANDALONE_COMMENT and leaf.bracket_depth <= depth_limit:
+                return True
 
         return False
 
-    def contains_multiline_strings(self) -> bool:
-        for leaf in self.leaves:
-            if is_multiline_string(leaf):
-                return True
+    def contains_uncollapsable_type_comments(self) -> bool:
+        ignored_ids = set()
+        try:
+            last_leaf = self.leaves[-1]
+            ignored_ids.add(id(last_leaf))
+            if last_leaf.type == token.COMMA or (
+                last_leaf.type == token.RPAR and not last_leaf.value
+            ):
+                # When trailing commas or optional parens are inserted by Black for
+                # consistency, comments after the previous last element are not moved
+                # (they don't have to, rendering will still be correct).  So we ignore
+                # trailing commas and invisible.
+                last_leaf = self.leaves[-2]
+                ignored_ids.add(id(last_leaf))
+        except IndexError:
+            return False
+
+        # A type comment is uncollapsable if it is attached to a leaf
+        # that isn't at the end of the line (since that could cause it
+        # to get associated to a different argument) or if there are
+        # comments before it (since that could cause it to get hidden
+        # behind a comment.
+        comment_seen = False
+        for leaf_id, comments in self.comments.items():
+            for comment in comments:
+                if is_type_comment(comment):
+                    if comment_seen or (
+                        not is_type_comment(comment, " ignore")
+                        and leaf_id not in ignored_ids
+                    ):
+                        return True
+
+                comment_seen = True
+
+        return False
+
+    def contains_unsplittable_type_ignore(self) -> bool:
+        if not self.leaves:
+            return False
+
+        # If a 'type: ignore' is attached to the end of a line, we
+        # can't split the line, because we can't know which of the
+        # subexpressions the ignore was meant to apply to.
+        #
+        # We only want this to apply to actual physical lines from the
+        # original source, though: we don't want the presence of a
+        # 'type: ignore' at the end of a multiline expression to
+        # justify pushing it all onto one line. Thus we
+        # (unfortunately) need to check the actual source lines and
+        # only report an unsplittable 'type: ignore' if this line was
+        # one line in the original code.
+
+        # Grab the first and last line numbers, skipping generated leaves
+        first_line = next((l.lineno for l in self.leaves if l.lineno != 0), 0)
+        last_line = next((l.lineno for l in reversed(self.leaves) if l.lineno != 0), 0)
+
+        if first_line == last_line:
+            # We look at the last two leaves since a comma or an
+            # invisible paren could have been added at the end of the
+            # line.
+            for node in self.leaves[-2:]:
+                for comment in self.comments.get(id(node), []):
+                    if is_type_comment(comment, " ignore"):
+                        return True
 
         return False
 
+    def contains_multiline_strings(self) -> bool:
+        return any(is_multiline_string(leaf) for leaf in self.leaves)
+
     def maybe_remove_trailing_comma(self, closing: Leaf) -> bool:
         """Remove trailing comma if there is one and it's safe."""
+        if not (self.leaves and self.leaves[-1].type == token.COMMA):
+            return False
+
+        # We remove trailing commas only in the case of importing a
+        # single name from a module.
         if not (
             self.leaves
+            and self.is_import
+            and len(self.leaves) > 4
             and self.leaves[-1].type == token.COMMA
             and closing.type in CLOSING_BRACKETS
+            and self.leaves[-4].type == token.NAME
+            and (
+                # regular `from foo import bar,`
+                self.leaves[-4].value == "import"
+                # `from foo import (bar as baz,)
+                or (
+                    len(self.leaves) > 6
+                    and self.leaves[-6].value == "import"
+                    and self.leaves[-3].value == "as"
+                )
+                # `from foo import bar as baz,`
+                or (
+                    len(self.leaves) > 5
+                    and self.leaves[-5].value == "import"
+                    and self.leaves[-3].value == "as"
+                )
+            )
+            and closing.type == token.RPAR
         ):
             return False
 
-        if closing.type == token.RBRACE:
-            self.remove_trailing_comma()
-            return True
-
-        if closing.type == token.RSQB:
-            comma = self.leaves[-1]
-            if comma.parent and comma.parent.type == syms.listmaker:
-                self.remove_trailing_comma()
-                return True
-
-        # For parens let's check if it's safe to remove the comma.
-        # Imports are always safe.
-        if self.is_import:
-            self.remove_trailing_comma()
-            return True
-
-        # Otherwise, if the trailing one is the only one, we might mistakenly
-        # change a tuple into a different type by removing the comma.
-        depth = closing.bracket_depth + 1
-        commas = 0
-        opening = closing.opening_bracket
-        for _opening_index, leaf in enumerate(self.leaves):
-            if leaf is opening:
-                break
-
-        else:
-            return False
-
-        for leaf in self.leaves[_opening_index + 1 :]:
-            if leaf is closing:
-                break
-
-            bracket_depth = leaf.bracket_depth
-            if bracket_depth == depth and leaf.type == token.COMMA:
-                commas += 1
-                if leaf.parent and leaf.parent.type == syms.arglist:
-                    commas += 1
-                    break
-
-        if commas > 1:
-            self.remove_trailing_comma()
-            return True
-
-        return False
+        self.remove_trailing_comma()
+        return True
 
     def append_comment(self, comment: Leaf) -> bool:
         """Add an inline or standalone comment to the line."""
@@ -1233,44 +1580,42 @@ class Line:
         if comment.type != token.COMMENT:
             return False
 
-        after = len(self.leaves) - 1
-        if after == -1:
+        if not self.leaves:
             comment.type = STANDALONE_COMMENT
             comment.prefix = ""
             return False
 
-        else:
-            self.comments.append((after, comment))
-            return True
-
-    def comments_after(self, leaf: Leaf, _index: int = -1) -> Iterator[Leaf]:
-        """Generate comments that should appear directly after `leaf`.
-
-        Provide a non-negative leaf `_index` to speed up the function.
-        """
-        if not self.comments:
-            return
-
-        if _index == -1:
-            for _index, _leaf in enumerate(self.leaves):
-                if leaf is _leaf:
-                    break
+        last_leaf = self.leaves[-1]
+        if (
+            last_leaf.type == token.RPAR
+            and not last_leaf.value
+            and last_leaf.parent
+            and len(list(last_leaf.parent.leaves())) <= 3
+            and not is_type_comment(comment)
+        ):
+            # Comments on an optional parens wrapping a single leaf should belong to
+            # the wrapped node except if it's a type comment. Pinning the comment like
+            # this avoids unstable formatting caused by comment migration.
+            if len(self.leaves) < 2:
+                comment.type = STANDALONE_COMMENT
+                comment.prefix = ""
+                return False
 
-            else:
-                return
+            last_leaf = self.leaves[-2]
+        self.comments.setdefault(id(last_leaf), []).append(comment)
+        return True
 
-        for index, comment_after in self.comments:
-            if _index == index:
-                yield comment_after
+    def comments_after(self, leaf: Leaf) -> List[Leaf]:
+        """Generate comments that should appear directly after `leaf`."""
+        return self.comments.get(id(leaf), [])
 
     def remove_trailing_comma(self) -> None:
         """Remove the trailing comma and moves the comments attached to it."""
-        comma_index = len(self.leaves) - 1
-        for i in range(len(self.comments)):
-            comment_index, comment = self.comments[i]
-            if comment_index == comma_index:
-                self.comments[i] = (comma_index - 1, comment)
-        self.leaves.pop()
+        trailing_comma = self.leaves.pop()
+        trailing_comma_comments = self.comments.pop(id(trailing_comma), [])
+        self.comments.setdefault(id(self.leaves[-1]), []).extend(
+            trailing_comma_comments
+        )
 
     def is_complex_subscript(self, leaf: Leaf) -> bool:
         """Return True iff `leaf` is part of a slice with non-trivial exprs."""
@@ -1290,6 +1635,13 @@ class Line:
             n.type in TEST_DESCENDANTS for n in subscript_start.pre_order()
         )
 
+    def clone(self) -> "Line":
+        return Line(
+            depth=self.depth,
+            inside_brackets=self.inside_brackets,
+            should_explode=self.should_explode,
+        )
+
     def __str__(self) -> str:
         """Render the line."""
         if not self:
@@ -1301,8 +1653,9 @@ class Line:
         res = f"{first.prefix}{indent}{first.value}"
         for leaf in leaves:
             res += str(leaf)
-        for _, comment in self.comments:
+        for comment in itertools.chain.from_iterable(self.comments.values()):
             res += str(comment)
+
         return res + "\n"
 
     def __bool__(self) -> bool:
@@ -1323,7 +1676,7 @@ class EmptyLineTracker:
     is_pyi: bool = False
     previous_line: Optional[Line] = None
     previous_after: int = 0
-    previous_defs: List[int] = Factory(list)
+    previous_defs: List[int] = field(default_factory=list)
 
     def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
         """Return the number of extra empty lines before and after the `current_line`.
@@ -1332,7 +1685,13 @@ class EmptyLineTracker:
         lines (two on module-level).
         """
         before, after = self._maybe_empty_lines(current_line)
-        before -= self.previous_after
+        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
+        )
         self.previous_after = after
         self.previous_line = current_line
         return before, after
@@ -1431,9 +1790,8 @@ class LineGenerator(Visitor[Line]):
 
     is_pyi: bool = False
     normalize_strings: bool = True
-    current_line: Line = Factory(Line)
+    current_line: Line = field(default_factory=Line)
     remove_u_prefix: bool = False
-    allow_underscores: bool = False
 
     def line(self, indent: int = 0) -> Iterator[Line]:
         """Generate a line.
@@ -1476,18 +1834,18 @@ class LineGenerator(Visitor[Line]):
                 normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
                 normalize_string_quotes(node)
             if node.type == token.NUMBER:
-                normalize_numeric_literal(node, self.allow_underscores)
+                normalize_numeric_literal(node)
             if node.type not in WHITESPACE:
                 self.current_line.append(node)
         yield from super().visit_default(node)
 
-    def visit_INDENT(self, node: Node) -> Iterator[Line]:
+    def visit_INDENT(self, node: Leaf) -> Iterator[Line]:
         """Increase indentation level, maybe yield a line."""
         # In blib2to3 INDENT never holds comments.
         yield from self.line(+1)
         yield from self.visit_default(node)
 
-    def visit_DEDENT(self, node: Node) -> Iterator[Line]:
+    def visit_DEDENT(self, node: Leaf) -> Iterator[Line]:
         """Decrease indentation level, maybe yield a line."""
         # The current line might still wait for trailing comments.  At DEDENT time
         # there won't be any (they would be prefixes on the preceding NEWLINE).
@@ -1580,7 +1938,36 @@ class LineGenerator(Visitor[Line]):
             yield from self.line()
         yield from self.visit_default(leaf)
 
-    def __attrs_post_init__(self) -> None:
+    def visit_factor(self, node: Node) -> Iterator[Line]:
+        """Force parentheses between a unary op and a binary power:
+
+        -2 ** 8 -> -(2 ** 8)
+        """
+        _operator, operand = node.children
+        if (
+            operand.type == syms.power
+            and len(operand.children) == 3
+            and operand.children[1].type == token.DOUBLESTAR
+        ):
+            lpar = Leaf(token.LPAR, "(")
+            rpar = Leaf(token.RPAR, ")")
+            index = operand.remove() or 0
+            node.insert_child(index, Node(syms.atom, [lpar, operand, rpar]))
+        yield from self.visit_default(node)
+
+    def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
+        # Check if it's a docstring
+        if prev_siblings_are(
+            leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt]
+        ) and is_multiline_string(leaf):
+            prefix = "    " * self.current_line.depth
+            docstring = fix_docstring(leaf.value[3:-3], prefix)
+            leaf.value = leaf.value[0:3] + docstring + leaf.value[-3:]
+            normalize_string_quotes(leaf)
+
+        yield from self.visit_default(leaf)
+
+    def __post_init__(self) -> None:
         """You are in a twisty little maze of passages."""
         v = self.visit_stmt
         Ø: Set[str] = set()
@@ -1600,6 +1987,7 @@ class LineGenerator(Visitor[Line]):
         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
 
@@ -1612,7 +2000,7 @@ BRACKETS = OPENING_BRACKETS | CLOSING_BRACKETS
 ALWAYS_NO_SPACE = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT}
 
 
-def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa C901
+def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
     """Return whitespace prefix if needed for the given `leaf`.
 
     `complex_subscript` signals whether the given leaf is part of a subscription
@@ -1669,7 +2057,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa C901
                     # that, too.
                     return prevp.prefix
 
-        elif prevp.type in STARS:
+        elif prevp.type in VARARGS_SPECIALS:
             if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS):
                 return NO
 
@@ -1759,7 +2147,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa C901
             if not prevp or prevp.type == token.LPAR:
                 return NO
 
-        elif prev.type in {token.EQUAL} | STARS:
+        elif prev.type in {token.EQUAL} | VARARGS_SPECIALS:
             return NO
 
     elif p.type == syms.decorator:
@@ -1860,6 +2248,22 @@ def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]:
     return None
 
 
+def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> bool:
+    """Return if the `node` and its previous siblings match types against the provided
+    list of tokens; the provided `node`has its type matched against the last element in
+    the list.  `None` can be used as the first element to declare that the start of the
+    list is anchored at the start of its parent's children."""
+    if not tokens:
+        return True
+    if tokens[-1] is None:
+        return node is None
+    if not node:
+        return False
+    if node.type != tokens[-1]:
+        return False
+    return prev_siblings_are(node.prev_sibling, tokens[:-1])
+
+
 def child_towards(ancestor: Node, descendant: LN) -> Optional[LN]:
     """Return the child of `ancestor` that contains `descendant`."""
     node: Optional[LN] = descendant
@@ -1893,7 +2297,7 @@ def container_of(leaf: Leaf) -> LN:
     return container
 
 
-def is_split_after_delimiter(leaf: Leaf, previous: Leaf = None) -> int:
+def is_split_after_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
     """Return the priority of the `leaf` delimiter, given a line break after it.
 
     The delimiter priorities returned here are from those delimiters that would
@@ -1907,7 +2311,7 @@ def is_split_after_delimiter(leaf: Leaf, previous: Leaf = None) -> int:
     return 0
 
 
-def is_split_before_delimiter(leaf: Leaf, previous: Leaf = None) -> int:
+def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority:
     """Return the priority of the `leaf` delimiter, given a line break before it.
 
     The delimiter priorities returned here are from those delimiters that would
@@ -2057,15 +2461,21 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]:
 
     consumed = 0
     nlines = 0
+    ignored_lines = 0
     for index, line in enumerate(prefix.split("\n")):
         consumed += len(line) + 1  # adding the length of the split '\n'
         line = line.lstrip()
         if not line:
             nlines += 1
         if not line.startswith("#"):
+            # Escaped newlines outside of a comment are not really newlines at
+            # all. We treat a single-line comment following an escaped newline
+            # as a simple trailing comment.
+            if line.endswith("\\"):
+                ignored_lines += 1
             continue
 
-        if index == 0 and not is_endmarker:
+        if index == ignored_lines and not is_endmarker:
             comment_type = token.COMMENT  # simple trailing comment
         else:
             comment_type = STANDALONE_COMMENT
@@ -2082,8 +2492,8 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]:
 def make_comment(content: str) -> str:
     """Return a consistently formatted comment from the given `content` string.
 
-    All comments (except for "##", "#!", "#:") should have a single space between
-    the hash sign and the content.
+    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.
     """
@@ -2093,43 +2503,57 @@ def make_comment(content: str) -> str:
 
     if content[0] == "#":
         content = content[1:]
-    if content and content[0] not in " !:#":
+    if content and content[0] not in " !:#'%":
         content = " " + content
     return "#" + content
 
 
-def split_line(
-    line: Line, line_length: int, inner: bool = False, py36: bool = False
+def transform_line(
+    line: Line,
+    line_length: int,
+    normalize_strings: bool,
+    features: Collection[Feature] = (),
 ) -> Iterator[Line]:
-    """Split a `line` into potentially many lines.
+    """Transform a `line`, potentially splitting it into many lines.
 
     They should fit in the allotted `line_length` but might not be able to.
-    `inner` signifies that there were a pair of brackets somewhere around the
-    current `line`, possibly transitively. This means we can fallback to splitting
-    by delimiters if the LHS/RHS don't yield any results.
 
-    If `py36` is True, splitting may generate syntax that is only compatible
-    with Python 3.6 and later.
+    `features` are syntactical features that may be used in the output.
     """
     if line.is_comment:
         yield line
         return
 
-    line_str = str(line).strip("\n")
-    if not line.should_explode and is_line_short_enough(
-        line, line_length=line_length, line_str=line_str
-    ):
-        yield line
-        return
+    line_str = line_to_string(line)
 
-    split_funcs: List[SplitFunc]
-    if line.is_def:
-        split_funcs = [left_hand_split]
+    def init_st(ST: Type[StringTransformer]) -> StringTransformer:
+        """Initialize StringTransformer"""
+        return ST(line_length, normalize_strings)
+
+    string_merge = init_st(StringMerger)
+    string_paren_strip = init_st(StringParenStripper)
+    string_split = init_st(StringSplitter)
+    string_paren_wrap = init_st(StringParenWrapper)
+
+    transformers: List[Transformer]
+    if (
+        not line.contains_uncollapsable_type_comments()
+        and not line.should_explode
+        and not line.is_collection_with_optional_trailing_comma
+        and (
+            is_line_short_enough(line, line_length=line_length, line_str=line_str)
+            or line.contains_unsplittable_type_ignore()
+        )
+    ):
+        # Only apply basic string preprocessing, since lines shouldn't be split here.
+        transformers = [string_merge, string_paren_strip]
+    elif line.is_def:
+        transformers = [left_hand_split]
     else:
 
-        def rhs(line: Line, py36: bool = False) -> Iterator[Line]:
+        def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]:
             for omit in generate_trailers_to_omit(line, line_length):
-                lines = list(right_hand_split(line, line_length, py36, omit=omit))
+                lines = list(right_hand_split(line, line_length, features, omit=omit))
                 if is_line_short_enough(lines[0], line_length=line_length):
                     yield from lines
                     return
@@ -2137,28 +2561,51 @@ def split_line(
             # All splits failed, best effort split with no omits.
             # This mostly happens to multiline strings that are by definition
             # reported as not fitting a single line.
-            yield from right_hand_split(line, py36)
+            # line_length=1 here was historically a bug that somehow became a feature.
+            # See #762 and #781 for the full story.
+            yield from right_hand_split(line, line_length=1, features=features)
 
         if line.inside_brackets:
-            split_funcs = [delimiter_split, standalone_comment_split, rhs]
+            transformers = [
+                string_merge,
+                string_paren_strip,
+                delimiter_split,
+                standalone_comment_split,
+                string_split,
+                string_paren_wrap,
+                rhs,
+            ]
         else:
-            split_funcs = [rhs]
-    for split_func in split_funcs:
+            transformers = [
+                string_merge,
+                string_paren_strip,
+                string_split,
+                string_paren_wrap,
+                rhs,
+            ]
+
+    for transform in transformers:
         # We are accumulating lines in `result` because we might want to abort
         # mission and return the original line in the end, or attempt a different
         # split altogether.
         result: List[Line] = []
         try:
-            for l in split_func(line, py36):
+            for l in transform(line, features):
                 if str(l).strip("\n") == line_str:
-                    raise CannotSplit("Split function returned an unchanged result")
+                    raise CannotTransform(
+                        "Line transformer returned an unchanged result"
+                    )
 
                 result.extend(
-                    split_line(l, line_length=line_length, inner=True, py36=py36)
+                    transform_line(
+                        l,
+                        line_length=line_length,
+                        normalize_strings=normalize_strings,
+                        features=features,
+                    )
                 )
-        except CannotSplit as cs:
+        except CannotTransform:
             continue
-
         else:
             yield from result
             break
@@ -2167,68 +2614,1993 @@ def split_line(
         yield line
 
 
-def left_hand_split(line: Line, py36: bool = False) -> 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.
-    Prefer RHS otherwise.  This is why this function is not symmetrical with
-    :func:`right_hand_split` which also handles optional parentheses.
+@dataclass  # type: ignore
+class StringTransformer(ABC):
     """
-    head = Line(depth=line.depth)
-    body = Line(depth=line.depth + 1, inside_brackets=True)
-    tail = Line(depth=line.depth)
-    tail_leaves: List[Leaf] = []
-    body_leaves: List[Leaf] = []
-    head_leaves: List[Leaf] = []
-    current_leaves = head_leaves
-    matching_bracket = None
-    for leaf in line.leaves:
-        if (
-            current_leaves is body_leaves
-            and leaf.type in CLOSING_BRACKETS
-            and leaf.opening_bracket is matching_bracket
-        ):
-            current_leaves = tail_leaves if body_leaves else head_leaves
-        current_leaves.append(leaf)
-        if current_leaves is head_leaves:
-            if leaf.type in OPENING_BRACKETS:
-                matching_bracket = leaf
-                current_leaves = body_leaves
-    # Since body is a new indent level, remove spurious leading whitespace.
-    if body_leaves:
-        normalize_prefix(body_leaves[0], inside_brackets=True)
-    # Build the new lines.
-    for result, leaves in (head, head_leaves), (body, body_leaves), (tail, tail_leaves):
-        for leaf in leaves:
-            result.append(leaf, preformatted=True)
-            for comment_after in line.comments_after(leaf):
-                result.append(comment_after, preformatted=True)
-    bracket_split_succeeded_or_raise(head, body, tail)
-    for result in (head, body, tail):
-        if result:
-            yield result
+    An implementation of the Transformer protocol that relies on its
+    subclasses overriding the template methods `do_match(...)` and
+    `do_transform(...)`.
 
+    This Transformer works exclusively on strings (for example, by merging
+    or splitting them).
 
-def right_hand_split(  # noqa C901
-    line: Line, line_length: int, py36: bool = False, omit: Collection[LeafID] = ()
-) -> Iterator[Line]:
-    """Split line into many lines, starting with the last matching bracket pair.
+    The following sections can be found among the docstrings of each concrete
+    StringTransformer subclass.
 
-    If the split was by optional parentheses, attempt splitting without them, too.
-    `omit` is a collection of closing bracket IDs that shouldn't be considered for
-    this split.
+    Requirements:
+        Which requirements must be met of the given Line for this
+        StringTransformer to be applied?
 
-    Note: running this function modifies `bracket_depth` on the leaves of `line`.
+    Transformations:
+        If the given Line meets all of the above requirments, which string
+        transformations can you expect to be applied to it by this
+        StringTransformer?
+
+    Collaborations:
+        What contractual agreements does this StringTransformer have with other
+        StringTransfomers? Such collaborations should be eliminated/minimized
+        as much as possible.
+    """
+
+    line_length: int
+    normalize_strings: bool
+
+    @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.
+        """
+
+    @abstractmethod
+    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+        """
+        Yields:
+            * Ok(new_line) where new_line is the new transformed line.
+                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.
+
+        Side Effects:
+            This method should NOT mutate @line directly, but it MAY mutate the
+            Line's underlying Node structure. (WARNING: If the underlying Node
+            structure IS altered, then this method should NOT be allowed to
+            yield an CannotTransform after that point.)
+        """
+
+    def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]:
+        """
+        StringTransformer instances have a call signature that mirrors that of
+        the Transformer type.
+
+        Raises:
+            CannotTransform(...) if the concrete StringTransformer class is unable
+            to transform @line.
+        """
+        # Optimization to avoid calling `self.do_match(...)` when the line does
+        # not contain any string.
+        if not any(leaf.type == token.STRING for leaf in line.leaves):
+            raise CannotTransform("There are no strings in this line.")
+
+        match_result = self.do_match(line)
+
+        if isinstance(match_result, Err):
+            cant_transform = match_result.err()
+            raise CannotTransform(
+                f"The string transformer {self.__class__.__name__} does not recognize"
+                " this line as one that it can transform."
+            ) from cant_transform
+
+        string_idx = match_result.ok()
+
+        for line_result in self.do_transform(line, string_idx):
+            if isinstance(line_result, Err):
+                cant_transform = line_result.err()
+                raise CannotTransform(
+                    "StringTransformer failed while attempting to transform string."
+                ) from cant_transform
+            line = line_result.ok()
+            yield line
+
+
+@dataclass
+class CustomSplit:
+    """A custom (i.e. manual) string split.
+
+    A single CustomSplit instance represents a single substring.
+
+    Examples:
+        Consider the following string:
+        ```
+        "Hi there friend."
+        " This is a custom"
+        f" string {split}."
+        ```
+
+        This string will correspond to the following three CustomSplit instances:
+        ```
+        CustomSplit(False, 16)
+        CustomSplit(False, 17)
+        CustomSplit(True, 16)
+        ```
+    """
+
+    has_prefix: bool
+    break_idx: int
+
+
+class CustomSplitMapMixin:
+    """
+    This mixin class is used to map merged strings to a sequence of
+    CustomSplits, which will then be used to re-split the strings iff none of
+    the resultant substrings go over the configured max line length.
+    """
+
+    _Key = Tuple[StringID, str]
+    _CUSTOM_SPLIT_MAP: Dict[_Key, Tuple[CustomSplit, ...]] = defaultdict(tuple)
+
+    @staticmethod
+    def _get_key(string: str) -> "CustomSplitMapMixin._Key":
+        """
+        Returns:
+            A unique identifier that is used internally to map @string to a
+            group of custom splits.
+        """
+        return (id(string), string)
+
+    def add_custom_splits(
+        self, string: str, custom_splits: Iterable[CustomSplit]
+    ) -> None:
+        """Custom Split Map Setter Method
+
+        Side Effects:
+            Adds a mapping from @string to the custom splits @custom_splits.
+        """
+        key = self._get_key(string)
+        self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits)
+
+    def pop_custom_splits(self, string: str) -> List[CustomSplit]:
+        """Custom Split Map Getter Method
+
+        Returns:
+            * A list of the custom splits that are mapped to @string, if any
+            exist.
+                OR
+            * [], otherwise.
+
+        Side Effects:
+            Deletes the mapping between @string and its associated custom
+            splits (which are returned to the caller).
+        """
+        key = self._get_key(string)
+
+        custom_splits = self._CUSTOM_SPLIT_MAP[key]
+        del self._CUSTOM_SPLIT_MAP[key]
+
+        return list(custom_splits)
+
+    def has_custom_splits(self, string: str) -> bool:
+        """
+        Returns:
+            True iff @string is associated with a set of custom splits.
+        """
+        key = self._get_key(string)
+        return key in self._CUSTOM_SPLIT_MAP
+
+
+class StringMerger(CustomSplitMapMixin, StringTransformer):
+    """StringTransformer that merges strings together.
+
+    Requirements:
+        (A) The line contains adjacent strings such that at most one substring
+        has inline comments AND none of those inline comments are pragmas AND
+        the set of all substring prefixes is either of length 1 or equal to
+        {"", "f"} AND none of the substrings are raw strings (i.e. are prefixed
+        with 'r').
+            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
+        (B) All line-continuation backslashes are removed from the target string.
+
+    Collaborations:
+        StringMerger provides custom split information to StringSplitter.
+    """
+
+    def do_match(self, line: Line) -> TMatchResult:
+        LL = line.leaves
+
+        is_valid_index = is_valid_index_factory(LL)
+
+        for (i, leaf) in enumerate(LL):
+            if (
+                leaf.type == token.STRING
+                and is_valid_index(i + 1)
+                and LL[i + 1].type == token.STRING
+            ):
+                return Ok(i)
+
+            if leaf.type == token.STRING and "\\\n" in leaf.value:
+                return Ok(i)
+
+        return TErr("This line has no strings that need merging.")
+
+    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+        new_line = line
+        rblc_result = self.__remove_backslash_line_continuation_chars(
+            new_line, string_idx
+        )
+        if isinstance(rblc_result, Ok):
+            new_line = rblc_result.ok()
+
+        msg_result = self.__merge_string_group(new_line, string_idx)
+        if isinstance(msg_result, Ok):
+            new_line = msg_result.ok()
+
+        if isinstance(rblc_result, Err) and isinstance(msg_result, Err):
+            msg_cant_transform = msg_result.err()
+            rblc_cant_transform = rblc_result.err()
+            cant_transform = CannotTransform(
+                "StringMerger failed to merge any strings in this line."
+            )
+
+            # Chain the errors together using `__cause__`.
+            msg_cant_transform.__cause__ = rblc_cant_transform
+            cant_transform.__cause__ = msg_cant_transform
+
+            yield Err(cant_transform)
+        else:
+            yield Ok(new_line)
+
+    @staticmethod
+    def __remove_backslash_line_continuation_chars(
+        line: Line, string_idx: int
+    ) -> TResult[Line]:
+        """
+        Merge strings that were split across multiple lines using
+        line-continuation backslashes.
+
+        Returns:
+            Ok(new_line), if @line contains backslash line-continuation
+            characters.
+                OR
+            Err(CannotTransform), otherwise.
+        """
+        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)
+        ):
+            return TErr(
+                f"String leaf {string_leaf} does not contain any backslash line"
+                " continuation characters."
+            )
+
+        new_line = line.clone()
+        new_line.comments = line.comments
+        append_leaves(new_line, line, LL)
+
+        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]:
+        """
+        Merges string group (i.e. set of adjacent strings) where the first
+        string in the group is `line.leaves[string_idx]`.
+
+        Returns:
+            Ok(new_line), if ALL of the validation checks found in
+            __validate_msg(...) pass.
+                OR
+            Err(CannotTransform), otherwise.
+        """
+        LL = line.leaves
+
+        is_valid_index = is_valid_index_factory(LL)
+
+        vresult = self.__validate_msg(line, string_idx)
+        if isinstance(vresult, Err):
+            return vresult
+
+        # 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
+
+        # We will place BREAK_MARK in between every two substrings that we
+        # merge. We will then later go through our final result and use the
+        # various instances of BREAK_MARK we find to add the right values to
+        # the custom split map.
+        BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@"
+
+        QUOTE = LL[string_idx].value[-1]
+
+        def make_naked(string: str, string_prefix: str) -> str:
+            """Strip @string (i.e. make it a "naked" string)
+
+            Pre-conditions:
+                * assert_is_leaf_string(@string)
+
+            Returns:
+                A string that is identical to @string except that
+                @string_prefix has been stripped, the surrounding QUOTE
+                characters have been removed, and any remaining QUOTE
+                characters have been escaped.
+            """
+            assert_is_leaf_string(string)
+
+            RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)"
+            naked_string = string[len(string_prefix) + 1 : -1]
+            naked_string = re.sub(
+                "(" + RE_EVEN_BACKSLASHES + ")" + QUOTE, r"\1\\" + QUOTE, naked_string
+            )
+            return naked_string
+
+        # Holds the CustomSplit objects that will later be added to the custom
+        # split map.
+        custom_splits = []
+
+        # Temporary storage for the 'has_prefix' part of the CustomSplit objects.
+        prefix_tracker = []
+
+        # Sets the 'prefix' variable. This is the prefix that the final merged
+        # string will have.
+        next_str_idx = string_idx
+        prefix = ""
+        while (
+            not prefix
+            and is_valid_index(next_str_idx)
+            and LL[next_str_idx].type == token.STRING
+        ):
+            prefix = get_string_prefix(LL[next_str_idx].value)
+            next_str_idx += 1
+
+        # The next loop merges the string group. The final string will be
+        # contained in 'S'.
+        #
+        # The following convenience variables are used:
+        #
+        #   S: string
+        #   NS: naked string
+        #   SS: next string
+        #   NSS: naked next string
+        S = ""
+        NS = ""
+        num_of_strings = 0
+        next_str_idx = string_idx
+        while is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING:
+            num_of_strings += 1
+
+            SS = LL[next_str_idx].value
+            next_prefix = get_string_prefix(SS)
+
+            # If this is an f-string group but this substring is not prefixed
+            # 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)
+
+            NSS = make_naked(SS, next_prefix)
+
+            has_prefix = bool(next_prefix)
+            prefix_tracker.append(has_prefix)
+
+            S = prefix + QUOTE + NS + NSS + BREAK_MARK + QUOTE
+            NS = make_naked(S, prefix)
+
+            next_str_idx += 1
+
+        S_leaf = Leaf(token.STRING, S)
+        if self.normalize_strings:
+            normalize_string_quotes(S_leaf)
+
+        # Fill the 'custom_splits' list with the appropriate CustomSplit objects.
+        temp_string = S_leaf.value[len(prefix) + 1 : -1]
+        for has_prefix in prefix_tracker:
+            mark_idx = temp_string.find(BREAK_MARK)
+            assert (
+                mark_idx >= 0
+            ), "Logic error while filling the custom string breakpoint cache."
+
+            temp_string = temp_string[mark_idx + len(BREAK_MARK) :]
+            breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1
+            custom_splits.append(CustomSplit(has_prefix, breakpoint_idx))
+
+        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])
+
+        self.add_custom_splits(string_leaf.value, custom_splits)
+        return Ok(new_line)
+
+    @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(...).
+
+        Returns:
+            * Ok(None), if ALL validation checks (listed below) pass.
+                OR
+            * Err(CannotTransform), if any of the following are true:
+                - The target string is not in a string group (i.e. it has no
+                  adjacent strings).
+                - The string group has more than one inline comment.
+                - The string group has an inline comment that appears to be a pragma.
+                - 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.
+        """
+        num_of_inline_string_comments = 0
+        set_of_prefixes = set()
+        num_of_strings = 0
+        for leaf in line.leaves[string_idx:]:
+            if leaf.type != token.STRING:
+                # If the string group is trailed by a comma, we count the
+                # comments trailing the comma to be one of the string group's
+                # comments.
+                if leaf.type == token.COMMA and id(leaf) in line.comments:
+                    num_of_inline_string_comments += 1
+                break
+
+            if has_triple_quotes(leaf.value):
+                return TErr("StringMerger does NOT merge multiline strings.")
+
+            num_of_strings += 1
+            prefix = get_string_prefix(leaf.value)
+            if "r" in prefix:
+                return TErr("StringMerger does NOT merge raw strings.")
+
+            set_of_prefixes.add(prefix)
+
+            if id(leaf) in line.comments:
+                num_of_inline_string_comments += 1
+                if contains_pragma_comment(line.comments[id(leaf)]):
+                    return TErr("Cannot merge strings which have pragma comments.")
+
+        if num_of_strings < 2:
+            return TErr(
+                f"Not enough strings to merge (num_of_strings={num_of_strings})."
+            )
+
+        if num_of_inline_string_comments > 1:
+            return TErr(
+                f"Too many inline string comments ({num_of_inline_string_comments})."
+            )
+
+        if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}:
+            return TErr(f"Too many different prefixes ({set_of_prefixes}).")
+
+        return Ok(None)
+
+
+class StringParenStripper(StringTransformer):
+    """StringTransformer that strips surrounding parentheses from strings.
+
+    Requirements:
+        The line contains a string which is surrounded by parentheses and:
+            - The target string is NOT the only argument to a function call).
+            - The RPAR is NOT followed by an attribute access (i.e. a dot).
+
+    Transformations:
+        The parentheses mentioned in the 'Requirements' section are stripped.
+
+    Collaborations:
+        StringParenStripper has its own inherent usefulness, but it is also
+        relied on to clean up the parentheses created by StringParenWrapper (in
+        the event that they are no longer needed).
+    """
+
+    def do_match(self, line: Line) -> TMatchResult:
+        LL = line.leaves
+
+        is_valid_index = is_valid_index_factory(LL)
+
+        for (idx, leaf) in enumerate(LL):
+            # Should be a string...
+            if leaf.type != token.STRING:
+                continue
+
+            # Should be preceded by a non-empty LPAR...
+            if (
+                not is_valid_index(idx - 1)
+                or LL[idx - 1].type != token.LPAR
+                or is_empty_lpar(LL[idx - 1])
+            ):
+                continue
+
+            # That LPAR should NOT be preceded by a function name or a closing
+            # bracket (which could be a function which returns a function or a
+            # list/dictionary that contains a function)...
+            if is_valid_index(idx - 2) and (
+                LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS
+            ):
+                continue
+
+            string_idx = idx
+
+            # Skip the string trailer, if one exists.
+            string_parser = StringParser()
+            next_idx = string_parser.parse(LL, string_idx)
+
+            # Should be followed by a non-empty RPAR...
+            if (
+                is_valid_index(next_idx)
+                and LL[next_idx].type == token.RPAR
+                and not is_empty_rpar(LL[next_idx])
+            ):
+                # That RPAR should NOT be followed by a '.' symbol.
+                if is_valid_index(next_idx + 1) and LL[next_idx + 1].type == token.DOT:
+                    continue
+
+                return Ok(string_idx)
+
+        return TErr("This line has no strings wrapped in parens.")
+
+    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+        LL = line.leaves
+
+        string_parser = StringParser()
+        rpar_idx = string_parser.parse(LL, string_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."
+                )
+
+        new_line = line.clone()
+        new_line.comments = line.comments.copy()
+
+        append_leaves(new_line, line, LL[: string_idx - 1])
+
+        string_leaf = Leaf(token.STRING, LL[string_idx].value)
+        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()
+
+        yield Ok(new_line)
+
+
+class BaseStringSplitter(StringTransformer):
+    """
+    Abstract class for StringTransformers which transform a Line's strings by splitting
+    them or placing them on their own lines where necessary to avoid going over
+    the configured line length.
+
+    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
+        * The target string is NOT a "pointless" string (i.e. a string that has
+        no parent or siblings).
+            AND
+        * The target string is not followed by an inline comment that appears
+        to be a pragma.
+            AND
+        * The target string is not a multiline (i.e. triple-quote) string.
+    """
+
+    @abstractmethod
+    def do_splitter_match(self, line: Line) -> TMatchResult:
+        """
+        BaseStringSplitter asks its clients to override this method instead of
+        `StringTransformer.do_match(...)`.
+
+        Follows the same protocol as `StringTransformer.do_match(...)`.
+
+        Refer to `help(StringTransformer.do_match)` for more information.
+        """
+
+    def do_match(self, line: Line) -> TMatchResult:
+        match_result = self.do_splitter_match(line)
+        if isinstance(match_result, Err):
+            return match_result
+
+        string_idx = match_result.ok()
+        vresult = self.__validate(line, string_idx)
+        if isinstance(vresult, Err):
+            return vresult
+
+        return match_result
+
+    def __validate(self, line: Line, string_idx: int) -> TResult[None]:
+        """
+        Checks that @line meets all of the requirements listed in this classes'
+        docstring. Refer to `help(BaseStringSplitter)` for a detailed
+        description of those requirements.
+
+        Returns:
+            * Ok(None), if ALL of the requirements are met.
+                OR
+            * Err(CannotTransform), if ANY of the requirements are NOT met.
+        """
+        LL = line.leaves
+
+        string_leaf = LL[string_idx]
+
+        max_string_length = self.__get_max_string_length(line, string_idx)
+        if len(string_leaf.value) <= max_string_length:
+            return TErr(
+                "The string itself is not what is causing this line to be too long."
+            )
+
+        if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [
+            token.STRING,
+            token.NEWLINE,
+        ]:
+            return TErr(
+                f"This string ({string_leaf.value}) appears to be pointless (i.e. has"
+                " no parent)."
+            )
+
+        if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment(
+            line.comments[id(line.leaves[string_idx])]
+        ):
+            return TErr(
+                "Line appears to end with an inline pragma comment. Splitting the line"
+                " could modify the pragma's behavior."
+            )
+
+        if has_triple_quotes(string_leaf.value):
+            return TErr("We cannot split multiline strings.")
+
+        return Ok(None)
+
+    def __get_max_string_length(self, line: Line, string_idx: int) -> int:
+        """
+        Calculates the max string length used when attempting to determine
+        whether or not the target string is responsible for causing the line to
+        go over the line length limit.
+
+        WARNING: This method is tightly coupled to both StringSplitter and
+        (especially) StringParenWrapper. There is probably a better way to
+        accomplish what is being done here.
+
+        Returns:
+            max_string_length: such that `line.leaves[string_idx].value >
+            max_string_length` implies that the target string IS responsible
+            for causing this line to exceed the line length limit.
+        """
+        LL = line.leaves
+
+        is_valid_index = is_valid_index_factory(LL)
+
+        # We use the shorthand "WMA4" in comments to abbreviate "We must
+        # account for". When giving examples, we use STRING to mean some/any
+        # valid string.
+        #
+        # Finally, we use the following convenience variables:
+        #
+        #   P:  The leaf that is before the target string leaf.
+        #   N:  The leaf that is after the target string leaf.
+        #   NN: The leaf that is after N.
+
+        # WMA4 the whitespace at the beginning of the line.
+        offset = line.depth * 4
+
+        if is_valid_index(string_idx - 1):
+            p_idx = string_idx - 1
+            if (
+                LL[string_idx - 1].type == token.LPAR
+                and LL[string_idx - 1].value == ""
+                and string_idx >= 2
+            ):
+                # If the previous leaf is an empty LPAR placeholder, we should skip it.
+                p_idx -= 1
+
+            P = LL[p_idx]
+            if P.type == token.PLUS:
+                # WMA4 a space and a '+' character (e.g. `+ STRING`).
+                offset += 2
+
+            if P.type == token.COMMA:
+                # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`].
+                offset += 3
+
+            if P.type in [token.COLON, token.EQUAL, token.NAME]:
+                # This conditional branch is meant to handle dictionary keys,
+                # variable assignments, 'return STRING' statement lines, and
+                # 'else STRING' ternary expression lines.
+
+                # WMA4 a single space.
+                offset += 1
+
+                # WMA4 the lengths of any leaves that came before that space.
+                for leaf in LL[: p_idx + 1]:
+                    offset += len(str(leaf))
+
+        if is_valid_index(string_idx + 1):
+            N = LL[string_idx + 1]
+            if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2:
+                # If the next leaf is an empty RPAR placeholder, we should skip it.
+                N = LL[string_idx + 2]
+
+            if N.type == token.COMMA:
+                # WMA4 a single comma at the end of the string (e.g `STRING,`).
+                offset += 1
+
+            if is_valid_index(string_idx + 2):
+                NN = LL[string_idx + 2]
+
+                if N.type == token.DOT and NN.type == token.NAME:
+                    # This conditional branch is meant to handle method calls invoked
+                    # off of a string literal up to and including the LPAR character.
+
+                    # WMA4 the '.' character.
+                    offset += 1
+
+                    if (
+                        is_valid_index(string_idx + 3)
+                        and LL[string_idx + 3].type == token.LPAR
+                    ):
+                        # WMA4 the left parenthesis character.
+                        offset += 1
+
+                    # WMA4 the length of the method's name.
+                    offset += len(NN.value)
+
+        has_comments = False
+        for comment_leaf in line.comments_after(LL[string_idx]):
+            if not has_comments:
+                has_comments = True
+                # WMA4 two spaces before the '#' character.
+                offset += 2
+
+            # WMA4 the length of the inline comment.
+            offset += len(comment_leaf.value)
+
+        max_string_length = self.line_length - offset
+        return max_string_length
+
+
+class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
+    """
+    StringTransformer that splits "atom" strings (i.e. strings which exist on
+    lines by themselves).
+
+    Requirements:
+        * The line consists ONLY of a single string (with the exception of a
+        '+' symbol which MAY exist at the start of the line), MAYBE a string
+        trailer, and MAYBE a trailing comma.
+            AND
+        * All of the requirements listed in BaseStringSplitter's docstring.
+
+    Transformations:
+        The string mentioned in the 'Requirements' section is split into as
+        many substrings as necessary to adhere to the configured line length.
+
+        In the final set of substrings, no substring should be smaller than
+        MIN_SUBSTR_SIZE characters.
+
+        The string will ONLY be split on spaces (i.e. each new substring should
+        start with a space).
+
+        If the string is an f-string, it will NOT be split in the middle of an
+        f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x
+        else bar()} is an f-expression).
+
+        If the string that is being split has an associated set of custom split
+        records and those custom splits will NOT result in any line going over
+        the configured line length, those custom splits are used. Otherwise the
+        string is split as late as possible (from left-to-right) while still
+        adhering to the transformation rules listed above.
+
+    Collaborations:
+        StringSplitter relies on StringMerger to construct the appropriate
+        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"""
+    (?<!\{)\{
+        (?:
+            [^\{\}]
+            | \{\{
+            | \}\}
+        )+?
+    (?<!\})(?:\}\})*\}(?!\})
+    """
+
+    def do_splitter_match(self, line: Line) -> TMatchResult:
+        LL = line.leaves
+
+        is_valid_index = is_valid_index_factory(LL)
+
+        idx = 0
+
+        # The first leaf MAY be a '+' symbol...
+        if is_valid_index(idx) and LL[idx].type == token.PLUS:
+            idx += 1
+
+        # The next/first leaf MAY be an empty LPAR...
+        if is_valid_index(idx) and is_empty_lpar(LL[idx]):
+            idx += 1
+
+        # The next/first leaf MUST be a string...
+        if not is_valid_index(idx) or LL[idx].type != token.STRING:
+            return TErr("Line does not start with a string.")
+
+        string_idx = idx
+
+        # Skip the string trailer, if one exists.
+        string_parser = StringParser()
+        idx = string_parser.parse(LL, string_idx)
+
+        # That string MAY be followed by an empty RPAR...
+        if is_valid_index(idx) and is_empty_rpar(LL[idx]):
+            idx += 1
+
+        # That string / empty RPAR leaf MAY be followed by a comma...
+        if is_valid_index(idx) and LL[idx].type == token.COMMA:
+            idx += 1
+
+        # But no more leaves are allowed...
+        if is_valid_index(idx):
+            return TErr("This line does not end with a string.")
+
+        return Ok(string_idx)
+
+    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+        LL = line.leaves
+
+        QUOTE = LL[string_idx].value[-1]
+
+        is_valid_index = is_valid_index_factory(LL)
+        insert_str_child = insert_str_child_factory(LL[string_idx])
+
+        prefix = get_string_prefix(LL[string_idx].value)
+
+        # We MAY choose to drop the 'f' prefix from substrings that don't
+        # contain any f-expressions, but ONLY if the original f-string
+        # containes 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
+        )
+
+        first_string_line = True
+        starts_with_plus = LL[0].type == token.PLUS
+
+        def line_needs_plus() -> bool:
+            return first_string_line and starts_with_plus
+
+        def maybe_append_plus(new_line: Line) -> None:
+            """
+            Side Effects:
+                If @line starts with a plus and this is the first line we are
+                constructing, this function appends a PLUS leaf to @new_line
+                and replaces the old PLUS leaf in the node structure. Otherwise
+                this function does nothing.
+            """
+            if line_needs_plus():
+                plus_leaf = Leaf(token.PLUS, "+")
+                replace_child(LL[0], plus_leaf)
+                new_line.append(plus_leaf)
+
+        ends_with_comma = (
+            is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
+        )
+
+        def max_last_string() -> int:
+            """
+            Returns:
+                The max allowed length of the string value used for the last
+                line we will construct.
+            """
+            result = self.line_length
+            result -= line.depth * 4
+            result -= 1 if ends_with_comma else 0
+            result -= 2 if line_needs_plus() else 0
+            return result
+
+        # --- Calculate Max Break Index (for string value)
+        # We start with the line length limit
+        max_break_idx = self.line_length
+        # The last index of a string of length N is N-1.
+        max_break_idx -= 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:
+            yield TErr(
+                f"Unable to split {LL[string_idx].value} at such high of a line depth:"
+                f" {line.depth}"
+            )
+            return
+
+        # Check if StringMerger registered any custom splits.
+        custom_splits = self.pop_custom_splits(LL[string_idx].value)
+        # We use them ONLY if none of them would produce lines that exceed the
+        # line limit.
+        use_custom_breakpoints = bool(
+            custom_splits
+            and all(csplit.break_idx <= max_break_idx for csplit in custom_splits)
+        )
+
+        # Temporary storage for the remaining chunk of the string line that
+        # can't fit onto the line currently being constructed.
+        rest_value = LL[string_idx].value
+
+        def more_splits_should_be_made() -> bool:
+            """
+            Returns:
+                True iff `rest_value` (the remaining string value from the last
+                split), should be split again.
+            """
+            if use_custom_breakpoints:
+                return len(custom_splits) > 1
+            else:
+                return len(rest_value) > max_last_string()
+
+        string_line_results: List[Ok[Line]] = []
+        while more_splits_should_be_made():
+            if use_custom_breakpoints:
+                # Custom User Split (manual)
+                csplit = custom_splits.pop(0)
+                break_idx = csplit.break_idx
+            else:
+                # Algorithmic Split (automatic)
+                max_bidx = max_break_idx - 2 if line_needs_plus() else max_break_idx
+                maybe_break_idx = self.__get_break_idx(rest_value, max_bidx)
+                if maybe_break_idx is None:
+                    # If we are unable to algorthmically determine a good split
+                    # and this string has custom splits registered to it, we
+                    # fall back to using them--which means we have to start
+                    # over from the beginning.
+                    if custom_splits:
+                        rest_value = LL[string_idx].value
+                        string_line_results = []
+                        first_string_line = True
+                        use_custom_breakpoints = True
+                        continue
+
+                    # Otherwise, we stop splitting here.
+                    break
+
+                break_idx = maybe_break_idx
+
+            # --- Construct `next_value`
+            next_value = rest_value[:break_idx] + QUOTE
+            if (
+                # Are we allowed to try to drop a pointless 'f' prefix?
+                drop_pointless_f_prefix
+                # If we are, will we be successful?
+                and next_value != self.__normalize_f_string(next_value, prefix)
+            ):
+                # If the current custom split did NOT originally use a prefix,
+                # then `csplit.break_idx` will be off by one after removing
+                # the 'f' prefix.
+                break_idx = (
+                    break_idx + 1
+                    if use_custom_breakpoints and not csplit.has_prefix
+                    else break_idx
+                )
+                next_value = rest_value[:break_idx] + QUOTE
+                next_value = self.__normalize_f_string(next_value, prefix)
+
+            # --- Construct `next_leaf`
+            next_leaf = Leaf(token.STRING, next_value)
+            insert_str_child(next_leaf)
+            self.__maybe_normalize_string_quotes(next_leaf)
+
+            # --- Construct `next_line`
+            next_line = line.clone()
+            maybe_append_plus(next_line)
+            next_line.append(next_leaf)
+            string_line_results.append(Ok(next_line))
+
+            rest_value = prefix + QUOTE + rest_value[break_idx:]
+            first_string_line = False
+
+        yield from string_line_results
+
+        if drop_pointless_f_prefix:
+            rest_value = self.__normalize_f_string(rest_value, prefix)
+
+        rest_leaf = Leaf(token.STRING, rest_value)
+        insert_str_child(rest_leaf)
+
+        # NOTE: I could not find a test case that verifies that the following
+        # line is actually necessary, but it seems to be. Otherwise we risk
+        # not normalizing the last substring, right?
+        self.__maybe_normalize_string_quotes(rest_leaf)
+
+        last_line = line.clone()
+        maybe_append_plus(last_line)
+
+        # If there are any leaves to the right of the target string...
+        if is_valid_index(string_idx + 1):
+            # We use `temp_value` here to determine how long the last line
+            # would be if we were to append all the leaves to the right of the
+            # target string to the last string line.
+            temp_value = rest_value
+            for leaf in LL[string_idx + 1 :]:
+                temp_value += str(leaf)
+                if leaf.type == token.LPAR:
+                    break
+
+            # Try to fit them all on the same line with the last substring...
+            if (
+                len(temp_value) <= max_last_string()
+                or LL[string_idx + 1].type == token.COMMA
+            ):
+                last_line.append(rest_leaf)
+                append_leaves(last_line, line, LL[string_idx + 1 :])
+                yield Ok(last_line)
+            # Otherwise, place the last substring on one line and everything
+            # else on a line below that...
+            else:
+                last_line.append(rest_leaf)
+                yield Ok(last_line)
+
+                non_string_line = line.clone()
+                append_leaves(non_string_line, line, LL[string_idx + 1 :])
+                yield Ok(non_string_line)
+        # Else the target string was the last leaf...
+        else:
+            last_line.append(rest_leaf)
+            last_line.comments = line.comments.copy()
+            yield Ok(last_line)
+
+    def __get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]:
+        """
+        This method contains the algorithm that StringSplitter uses to
+        determine which character to split each string at.
+
+        Args:
+            @string: The substring that we are attempting to split.
+            @max_break_idx: The ideal break index. We will return this value if it
+            meets all the necessary conditions. In the likely event that it
+            doesn't we will try to find the closest index BELOW @max_break_idx
+            that does. If that fails, we will expand our search by also
+            considering all valid indices ABOVE @max_break_idx.
+
+        Pre-Conditions:
+            * assert_is_leaf_string(@string)
+            * 0 <= @max_break_idx < len(@string)
+
+        Returns:
+            break_idx, if an index is able to be found that meets all of the
+            conditions listed in the 'Transformations' section of this classes'
+            docstring.
+                OR
+            None, otherwise.
+        """
+        is_valid_index = is_valid_index_factory(string)
+
+        assert is_valid_index(max_break_idx)
+        assert_is_leaf_string(string)
+
+        _fexpr_slices: Optional[List[Tuple[Index, Index]]] = None
+
+        def fexpr_slices() -> Iterator[Tuple[Index, Index]]:
+            """
+            Yields:
+                All ranges of @string which, if @string were to be split there,
+                would result in the splitting of an f-expression (which is NOT
+                allowed).
+            """
+            nonlocal _fexpr_slices
+
+            if _fexpr_slices is None:
+                _fexpr_slices = []
+                for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE):
+                    _fexpr_slices.append(match.span())
+
+            yield from _fexpr_slices
+
+        is_fstring = "f" in get_string_prefix(string)
+
+        def breaks_fstring_expression(i: Index) -> bool:
+            """
+            Returns:
+                True iff returning @i would result in the splitting of an
+                f-expression (which is NOT allowed).
+            """
+            if not is_fstring:
+                return False
+
+            for (start, end) in fexpr_slices():
+                if start <= i < end:
+                    return True
+
+            return False
+
+        def passes_all_checks(i: Index) -> bool:
+            """
+            Returns:
+                True iff ALL of the conditions listed in the 'Transformations'
+                section of this classes' docstring would be be met by returning @i.
+            """
+            is_space = string[i] == " "
+            is_big_enough = (
+                len(string[i:]) >= self.MIN_SUBSTR_SIZE
+                and len(string[:i]) >= self.MIN_SUBSTR_SIZE
+            )
+            return is_space and is_big_enough and not breaks_fstring_expression(i)
+
+        # First, we check all indices BELOW @max_break_idx.
+        break_idx = max_break_idx
+        while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx):
+            break_idx -= 1
+
+        if not passes_all_checks(break_idx):
+            # If that fails, we check all indices ABOVE @max_break_idx.
+            #
+            # If we are able to find a valid index here, the next line is going
+            # to be longer than the specified line length, but it's probably
+            # better than doing nothing at all.
+            break_idx = max_break_idx + 1
+            while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx):
+                break_idx += 1
+
+            if not is_valid_index(break_idx) or not passes_all_checks(break_idx):
+                return None
+
+        return break_idx
+
+    def __maybe_normalize_string_quotes(self, leaf: Leaf) -> None:
+        if self.normalize_strings:
+            normalize_string_quotes(leaf)
+
+    def __normalize_f_string(self, string: str, prefix: str) -> str:
+        """
+        Pre-Conditions:
+            * assert_is_leaf_string(@string)
+
+        Returns:
+            * If @string is an f-string that contains no f-expressions, we
+            return a string identical to @string except that the 'f' prefix
+            has been stripped and all double braces (i.e. '{{' or '}}') have
+            been normalized (i.e. turned into '{' or '}').
+                OR
+            * Otherwise, we return @string.
+        """
+        assert_is_leaf_string(string)
+
+        if "f" in prefix and not re.search(self.RE_FEXPR, string, re.VERBOSE):
+            new_prefix = prefix.replace("f", "")
+
+            temp = string[len(prefix) :]
+            temp = re.sub(r"\{\{", "{", temp)
+            temp = re.sub(r"\}\}", "}", temp)
+            new_string = temp
+
+            return f"{new_prefix}{new_string}"
+        else:
+            return string
+
+
+class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
+    """
+    StringTransformer that splits non-"atom" strings (i.e. strings that do not
+    exist on lines by themselves).
+
+    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
+        * 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
+        * The line is an assert statement, which ends with a string.
+            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
+        * The line is a dictionary key assignment where some valid key is being
+        assigned the value of some string.
+
+    Transformations:
+        The chosen string is wrapped in parentheses and then split at the LPAR.
+
+        We then have one line which ends with an LPAR and another line that
+        starts with the chosen string. The latter line is then split again at
+        the RPAR. This results in the RPAR (and possibly a trailing comma)
+        being placed on its own line.
+
+        NOTE: If any leaves exist to the right of the chosen string (except
+        for a trailing comma, which would be placed after the RPAR), those
+        leaves are placed inside the parentheses.  In effect, the chosen
+        string is not necessarily being "wrapped" by parentheses. We can,
+        however, count on the LPAR being placed directly before the chosen
+        string.
+
+        In other words, StringParenWrapper creates "atom" strings. These
+        can then be split again by StringSplitter, if necessary.
+
+    Collaborations:
+        In the event that a string line split by StringParenWrapper is
+        changed such that it no longer needs to be given its own line,
+        StringParenWrapper relies on StringParenStripper to clean up the
+        parentheses it created.
+    """
+
+    def do_splitter_match(self, line: Line) -> TMatchResult:
+        LL = line.leaves
+
+        string_idx = None
+        string_idx = string_idx or self._return_match(LL)
+        string_idx = string_idx or self._else_match(LL)
+        string_idx = string_idx or self._assert_match(LL)
+        string_idx = string_idx or self._assign_match(LL)
+        string_idx = string_idx or self._dict_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:
+                # 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:
+                    # 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.
+                        return TErr(
+                            "We do not wrap long strings in parentheses when the"
+                            " resultant line would still be over the specified line"
+                            " length and can't be split further by StringSplitter."
+                        )
+            return Ok(string_idx)
+
+        return TErr("This line does not contain any non-atomic strings.")
+
+    @staticmethod
+    def _return_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 return/yield statement
+            requirements listed in the 'Requirements' section of this classes'
+            docstring.
+                OR
+            None, otherwise.
+        """
+        # If this line is apart of a return/yield statement and the first leaf
+        # contains either the "return" or "yield" keywords...
+        if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[
+            0
+        ].value in ["return", "yield"]:
+            is_valid_index = is_valid_index_factory(LL)
+
+            idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
+            # The next visible leaf MUST contain a string...
+            if is_valid_index(idx) and LL[idx].type == token.STRING:
+                return idx
+
+        return None
+
+    @staticmethod
+    def _else_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 ternary expression
+            requirements listed in the 'Requirements' section of this classes'
+            docstring.
+                OR
+            None, otherwise.
+        """
+        # If this line is apart of a ternary expression and the first leaf
+        # contains the "else" keyword...
+        if (
+            parent_type(LL[0]) == syms.test
+            and LL[0].type == token.NAME
+            and LL[0].value == "else"
+        ):
+            is_valid_index = is_valid_index_factory(LL)
+
+            idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
+            # The next visible leaf MUST contain a string...
+            if is_valid_index(idx) and LL[idx].type == token.STRING:
+                return idx
+
+        return None
+
+    @staticmethod
+    def _assert_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 assert statement
+            requirements listed in the 'Requirements' section of this classes'
+            docstring.
+                OR
+            None, otherwise.
+        """
+        # If this line is apart of an assert statement and the first leaf
+        # contains the "assert" keyword...
+        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):
+                # We MUST find a comma...
+                if leaf.type == token.COMMA:
+                    idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
+
+                    # That comma MUST be followed by a string...
+                    if is_valid_index(idx) and LL[idx].type == token.STRING:
+                        string_idx = idx
+
+                        # Skip the string trailer, if one exists.
+                        string_parser = StringParser()
+                        idx = string_parser.parse(LL, string_idx)
+
+                        # But no more leaves are allowed...
+                        if not is_valid_index(idx):
+                            return string_idx
+
+        return None
+
+    @staticmethod
+    def _assign_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 assignment statement
+            requirements listed in the 'Requirements' section of this classes'
+            docstring.
+                OR
+            None, otherwise.
+        """
+        # If this line is apart of an expression statement or is a function
+        # argument AND the first leaf contains a variable name...
+        if (
+            parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power]
+            and LL[0].type == token.NAME
+        ):
+            is_valid_index = is_valid_index_factory(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
+
+                    # That symbol MUST be followed by a string...
+                    if is_valid_index(idx) and LL[idx].type == token.STRING:
+                        string_idx = idx
+
+                        # Skip the string trailer, if one exists.
+                        string_parser = StringParser()
+                        idx = string_parser.parse(LL, string_idx)
+
+                        # The next leaf MAY be a comma iff this line is apart
+                        # of a function argument...
+                        if (
+                            parent_type(LL[0]) == syms.argument
+                            and is_valid_index(idx)
+                            and LL[idx].type == token.COMMA
+                        ):
+                            idx += 1
+
+                        # But no more leaves are allowed...
+                        if not is_valid_index(idx):
+                            return string_idx
+
+        return None
+
+    @staticmethod
+    def _dict_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.
+                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)]:
+            is_valid_index = is_valid_index_factory(LL)
+
+            for (i, leaf) in enumerate(LL):
+                # We MUST find a colon...
+                if leaf.type == token.COLON:
+                    idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
+
+                    # That colon MUST be followed by a string...
+                    if is_valid_index(idx) and LL[idx].type == token.STRING:
+                        string_idx = idx
+
+                        # Skip the string trailer, if one exists.
+                        string_parser = StringParser()
+                        idx = string_parser.parse(LL, string_idx)
+
+                        # That string MAY be followed by a comma...
+                        if is_valid_index(idx) and LL[idx].type == token.COMMA:
+                            idx += 1
+
+                        # But no more leaves are allowed...
+                        if not is_valid_index(idx):
+                            return string_idx
+
+        return None
+
+    def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
+        LL = line.leaves
+
+        is_valid_index = is_valid_index_factory(LL)
+        insert_str_child = insert_str_child_factory(LL[string_idx])
+
+        comma_idx = len(LL) - 1
+        ends_with_comma = False
+        if LL[comma_idx].type == token.COMMA:
+            ends_with_comma = True
+
+        leaves_to_steal_comments_from = [LL[string_idx]]
+        if ends_with_comma:
+            leaves_to_steal_comments_from.append(LL[comma_idx])
+
+        # --- First Line
+        first_line = line.clone()
+        left_leaves = LL[:string_idx]
+
+        # We have to remember to account for (possibly invisible) LPAR and RPAR
+        # leaves that already wrapped the target string. If these leaves do
+        # exist, we will replace them with our own LPAR and RPAR leaves.
+        old_parens_exist = False
+        if left_leaves and left_leaves[-1].type == token.LPAR:
+            old_parens_exist = True
+            leaves_to_steal_comments_from.append(left_leaves[-1])
+            left_leaves.pop()
+
+        append_leaves(first_line, line, left_leaves)
+
+        lpar_leaf = Leaf(token.LPAR, "(")
+        if old_parens_exist:
+            replace_child(LL[string_idx - 1], lpar_leaf)
+        else:
+            insert_str_child(lpar_leaf)
+        first_line.append(lpar_leaf)
+
+        # We throw inline comments that were originally to the right of the
+        # target string to the top line. They will now be shown to the right of
+        # the LPAR.
+        for leaf in leaves_to_steal_comments_from:
+            for comment_leaf in line.comments_after(leaf):
+                first_line.append(comment_leaf, preformatted=True)
+
+        yield Ok(first_line)
+
+        # --- Middle (String) Line
+        # We only need to yield one (possibly too long) string line, since the
+        # `StringSplitter` will break it down further if necessary.
+        string_value = LL[string_idx].value
+        string_line = Line(
+            depth=line.depth + 1,
+            inside_brackets=True,
+            should_explode=line.should_explode,
+        )
+        string_leaf = Leaf(token.STRING, string_value)
+        insert_str_child(string_leaf)
+        string_line.append(string_leaf)
+
+        old_rpar_leaf = None
+        if is_valid_index(string_idx + 1):
+            right_leaves = LL[string_idx + 1 :]
+            if ends_with_comma:
+                right_leaves.pop()
+
+            if old_parens_exist:
+                assert (
+                    right_leaves and right_leaves[-1].type == token.RPAR
+                ), "Apparently, old parentheses do NOT exist?!"
+                old_rpar_leaf = right_leaves.pop()
+
+            append_leaves(string_line, line, right_leaves)
+
+        yield Ok(string_line)
+
+        # --- Last Line
+        last_line = line.clone()
+        last_line.bracket_tracker = first_line.bracket_tracker
+
+        new_rpar_leaf = Leaf(token.RPAR, ")")
+        if old_rpar_leaf is not None:
+            replace_child(old_rpar_leaf, new_rpar_leaf)
+        else:
+            insert_str_child(new_rpar_leaf)
+        last_line.append(new_rpar_leaf)
+
+        # If the target string ended with a comma, we place this comma to the
+        # right of the RPAR on the last line.
+        if ends_with_comma:
+            comma_leaf = Leaf(token.COMMA, ",")
+            replace_child(LL[comma_idx], comma_leaf)
+            last_line.append(comma_leaf)
+
+        yield Ok(last_line)
+
+
+class StringParser:
+    """
+    A state machine that aids in parsing a string's "trailer", which can be
+    either non-existant, an old-style formatting sequence (e.g. `% varX` or `%
+    (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
+    varY)`).
+
+    NOTE: A new StringParser object MUST be instantiated for each string
+    trailer we need to parse.
+
+    Examples:
+        We shall assume that `line` equals the `Line` object that corresponds
+        to the following line of python code:
+        ```
+        x = "Some {}.".format("String") + some_other_string
+        ```
+
+        Furthermore, we will assume that `string_idx` is some index such that:
+        ```
+        assert line.leaves[string_idx].value == "Some {}."
+        ```
+
+        The following code snippet then holds:
+        ```
+        string_parser = StringParser()
+        idx = string_parser.parse(line.leaves, string_idx)
+        assert line.leaves[idx].type == token.PLUS
+        ```
+    """
+
+    DEFAULT_TOKEN = -1
+
+    # String Parser States
+    START = 1
+    DOT = 2
+    NAME = 3
+    PERCENT = 4
+    SINGLE_FMT_ARG = 5
+    LPAR = 6
+    RPAR = 7
+    DONE = 8
+
+    # Lookup Table for Next State
+    _goto: Dict[Tuple[ParserState, NodeType], ParserState] = {
+        # A string trailer may start with '.' OR '%'.
+        (START, token.DOT): DOT,
+        (START, token.PERCENT): PERCENT,
+        (START, DEFAULT_TOKEN): DONE,
+        # A '.' MUST be followed by an attribute or method name.
+        (DOT, token.NAME): NAME,
+        # A method name MUST be followed by an '(', whereas an attribute name
+        # is the last symbol in the string trailer.
+        (NAME, token.LPAR): LPAR,
+        (NAME, DEFAULT_TOKEN): DONE,
+        # A '%' symbol can be followed by an '(' or a single argument (e.g. a
+        # string or variable name).
+        (PERCENT, token.LPAR): LPAR,
+        (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG,
+        # If a '%' symbol is followed by a single argument, that argument is
+        # the last leaf in the string trailer.
+        (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE,
+        # If present, a ')' symbol is the last symbol in a string trailer.
+        # (NOTE: LPARS and nested RPARS are not included in this lookup table,
+        # since they are treated as a special case by the parsing logic in this
+        # classes' implementation.)
+        (RPAR, DEFAULT_TOKEN): DONE,
+    }
+
+    def __init__(self) -> None:
+        self._state = self.START
+        self._unmatched_lpars = 0
+
+    def parse(self, leaves: List[Leaf], string_idx: int) -> int:
+        """
+        Pre-conditions:
+            * @leaves[@string_idx].type == token.STRING
+
+        Returns:
+            The index directly after the last leaf which is apart of the string
+            trailer, if a "trailer" exists.
+                OR
+            @string_idx + 1, if no string "trailer" exists.
+        """
+        assert leaves[string_idx].type == token.STRING
+
+        idx = string_idx + 1
+        while idx < len(leaves) and self._next_state(leaves[idx]):
+            idx += 1
+        return idx
+
+    def _next_state(self, leaf: Leaf) -> bool:
+        """
+        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]`).
+            * On the next call to this function, the leaf paramater passed in
+            MUST be the leaf directly following @leaf.
+
+        Returns:
+            True iff @leaf is apart of the string's trailer.
+        """
+        # We ignore empty LPAR or RPAR leaves.
+        if is_empty_par(leaf):
+            return True
+
+        next_token = leaf.type
+        if next_token == token.LPAR:
+            self._unmatched_lpars += 1
+
+        current_state = self._state
+
+        # The LPAR parser state is a special case. We will return True until we
+        # find the matching RPAR token.
+        if current_state == self.LPAR:
+            if next_token == token.RPAR:
+                self._unmatched_lpars -= 1
+                if self._unmatched_lpars == 0:
+                    self._state = self.RPAR
+        # Otherwise, we use a lookup table to determine the next state.
+        else:
+            # If the lookup table matches the current state to the next
+            # token, we use the lookup table.
+            if (current_state, next_token) in self._goto:
+                self._state = self._goto[current_state, next_token]
+            else:
+                # Otherwise, we check if a the current state was assigned a
+                # default.
+                if (current_state, self.DEFAULT_TOKEN) in self._goto:
+                    self._state = self._goto[current_state, self.DEFAULT_TOKEN]
+                # If no default has been assigned, then this parser has a logic
+                # error.
+                else:
+                    raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
+
+            if self._state == self.DONE:
+                return False
+
+        return True
+
+
+def TErr(err_msg: str) -> Err[CannotTransform]:
+    """(T)ransform Err
+
+    Convenience function used when working with the TResult type.
+    """
+    cant_transform = CannotTransform(err_msg)
+    return Err(cant_transform)
+
+
+def contains_pragma_comment(comment_list: List[Leaf]) -> bool:
+    """
+    Returns:
+        True iff one of the comments in @comment_list is a pragma used by one
+        of the more common static analysis tools for python (e.g. mypy, flake8,
+        pylint).
+    """
+    for comment in comment_list:
+        if comment.value.startswith(("# type:", "# noqa", "# pylint:")):
+            return True
+
+    return False
+
+
+def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]:
+    """
+    Factory for a convenience function that is used to orphan @string_leaf
+    and then insert multiple new leaves into the same part of the node
+    structure that @string_leaf had originally occupied.
+
+    Examples:
+        Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N =
+        string_leaf.parent`. Assume the node `N` has the following
+        original structure:
+
+        Node(
+            expr_stmt, [
+                Leaf(NAME, 'x'),
+                Leaf(EQUAL, '='),
+                Leaf(STRING, '"foo"'),
+            ]
+        )
+
+        We then run the code snippet shown below.
+        ```
+        insert_str_child = insert_str_child_factory(string_leaf)
+
+        lpar = Leaf(token.LPAR, '(')
+        insert_str_child(lpar)
+
+        bar = Leaf(token.STRING, '"bar"')
+        insert_str_child(bar)
+
+        rpar = Leaf(token.RPAR, ')')
+        insert_str_child(rpar)
+        ```
+
+        After which point, it follows that `string_leaf.parent is None` and
+        the node `N` now has the following structure:
+
+        Node(
+            expr_stmt, [
+                Leaf(NAME, 'x'),
+                Leaf(EQUAL, '='),
+                Leaf(LPAR, '('),
+                Leaf(STRING, '"bar"'),
+                Leaf(RPAR, ')'),
+            ]
+        )
+    """
+    string_parent = string_leaf.parent
+    string_child_idx = string_leaf.remove()
+
+    def insert_str_child(child: LN) -> None:
+        nonlocal string_child_idx
+
+        assert string_parent is not None
+        assert string_child_idx is not None
+
+        string_parent.insert_child(string_child_idx, child)
+        string_child_idx += 1
+
+    return insert_str_child
+
+
+def has_triple_quotes(string: str) -> bool:
+    """
+    Returns:
+        True iff @string starts with three quotation characters.
+    """
+    raw_string = string.lstrip(STRING_PREFIX_CHARS)
+    return raw_string[:3] in {'"""', "'''"}
+
+
+def parent_type(node: Optional[LN]) -> Optional[NodeType]:
+    """
+    Returns:
+        @node.parent.type, if @node is not None and has a parent.
+            OR
+        None, otherwise.
+    """
+    if node is None or node.parent is None:
+        return None
+
+    return node.parent.type
+
+
+def is_empty_par(leaf: Leaf) -> bool:
+    return is_empty_lpar(leaf) or is_empty_rpar(leaf)
+
+
+def is_empty_lpar(leaf: Leaf) -> bool:
+    return leaf.type == token.LPAR and leaf.value == ""
+
+
+def is_empty_rpar(leaf: Leaf) -> bool:
+    return leaf.type == token.RPAR and leaf.value == ""
+
+
+def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]:
+    """
+    Examples:
+        ```
+        my_list = [1, 2, 3]
+
+        is_valid_index = is_valid_index_factory(my_list)
+
+        assert is_valid_index(0)
+        assert is_valid_index(2)
+
+        assert not is_valid_index(3)
+        assert not is_valid_index(-1)
+        ```
+    """
+
+    def is_valid_index(idx: int) -> bool:
+        """
+        Returns:
+            True iff @idx is positive AND seq[@idx] does NOT raise an
+            IndexError.
+        """
+        return 0 <= idx < len(seq)
+
+    return is_valid_index
+
+
+def line_to_string(line: Line) -> str:
+    """Returns the string representation of @line.
+
+    WARNING: This is known to be computationally expensive.
+    """
+    return str(line).strip("\n")
+
+
+def append_leaves(new_line: Line, old_line: Line, leaves: List[Leaf]) -> None:
+    """
+    Append leaves (taken from @old_line) to @new_line, making sure to fix the
+    underlying Node structure where appropriate.
+
+    All of the leaves in @leaves are duplicated. The duplicates are then
+    appended to @new_line and used to replace their originals in the underlying
+    Node structure. Any comments attatched to the old leaves are reattached to
+    the new leaves.
+
+    Pre-conditions:
+        set(@leaves) is a subset of set(@old_line.leaves).
+    """
+    for old_leaf in leaves:
+        assert old_leaf in old_line.leaves
+
+        new_leaf = Leaf(old_leaf.type, old_leaf.value)
+        replace_child(old_leaf, new_leaf)
+        new_line.append(new_leaf)
+
+        for comment_leaf in old_line.comments_after(old_leaf):
+            new_line.append(comment_leaf, preformatted=True)
+
+
+def replace_child(old_child: LN, new_child: LN) -> None:
+    """
+    Side Effects:
+        * If @old_child.parent is set, replace @old_child with @new_child in
+        @old_child's underlying Node structure.
+            OR
+        * Otherwise, this function does nothing.
+    """
+    parent = old_child.parent
+    if not parent:
+        return
+
+    child_idx = old_child.remove()
+    if child_idx is not None:
+        parent.insert_child(child_idx, new_child)
+
+
+def get_string_prefix(string: str) -> str:
+    """
+    Pre-conditions:
+        * assert_is_leaf_string(@string)
+
+    Returns:
+        @string's prefix (e.g. '', 'r', 'f', or 'rf').
+    """
+    assert_is_leaf_string(string)
+
+    prefix = ""
+    prefix_idx = 0
+    while string[prefix_idx] in STRING_PREFIX_CHARS:
+        prefix += string[prefix_idx].lower()
+        prefix_idx += 1
+
+    return prefix
+
+
+def assert_is_leaf_string(string: str) -> None:
+    """
+    Checks the pre-condition that @string has the format that you would expect
+    of `leaf.value` where `leaf` is some Leaf such that `leaf.type ==
+    token.STRING`. A more precise description of the pre-conditions that are
+    checked are listed below.
+
+    Pre-conditions:
+        * @string starts with either ', ", <prefix>', or <prefix>" where
+        `set(<prefix>)` is some subset of `set(STRING_PREFIX_CHARS)`.
+        * @string ends with a quote character (' or ").
+
+    Raises:
+        AssertionError(...) if the pre-conditions listed above are not
+        satisfied.
+    """
+    dquote_idx = string.find('"')
+    squote_idx = string.find("'")
+    if -1 in [dquote_idx, squote_idx]:
+        quote_idx = max(dquote_idx, squote_idx)
+    else:
+        quote_idx = min(squote_idx, dquote_idx)
+
+    assert (
+        0 <= quote_idx < len(string) - 1
+    ), f"{string!r} is missing a starting quote character (' or \")."
+    assert string[-1] in (
+        "'",
+        '"',
+    ), f"{string!r} is missing an ending quote character (' or \")."
+    assert set(string[:quote_idx]).issubset(
+        set(STRING_PREFIX_CHARS)
+    ), f"{set(string[:quote_idx])} is NOT a subset of {set(STRING_PREFIX_CHARS)}."
+
+
+def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> 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.
+    Prefer RHS otherwise.  This is why this function is not symmetrical with
+    :func:`right_hand_split` which also handles optional parentheses.
+    """
+    tail_leaves: List[Leaf] = []
+    body_leaves: List[Leaf] = []
+    head_leaves: List[Leaf] = []
+    current_leaves = head_leaves
+    matching_bracket: Optional[Leaf] = None
+    for leaf in line.leaves:
+        if (
+            current_leaves is body_leaves
+            and leaf.type in CLOSING_BRACKETS
+            and leaf.opening_bracket is matching_bracket
+        ):
+            current_leaves = tail_leaves if body_leaves else head_leaves
+        current_leaves.append(leaf)
+        if current_leaves is head_leaves:
+            if leaf.type in OPENING_BRACKETS:
+                matching_bracket = leaf
+                current_leaves = body_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)
+    bracket_split_succeeded_or_raise(head, body, tail)
+    for result in (head, body, tail):
+        if result:
+            yield result
+
+
+def right_hand_split(
+    line: Line,
+    line_length: int,
+    features: Collection[Feature] = (),
+    omit: Collection[LeafID] = (),
+) -> Iterator[Line]:
+    """Split line into many lines, starting with the last matching bracket pair.
+
+    If the split was by optional parentheses, attempt splitting without them, too.
+    `omit` is a collection of closing bracket IDs that shouldn't be considered for
+    this split.
+
+    Note: running this function modifies `bracket_depth` on the leaves of `line`.
     """
-    head = Line(depth=line.depth)
-    body = Line(depth=line.depth + 1, inside_brackets=True)
-    tail = Line(depth=line.depth)
     tail_leaves: List[Leaf] = []
     body_leaves: List[Leaf] = []
     head_leaves: List[Leaf] = []
     current_leaves = tail_leaves
-    opening_bracket = None
-    closing_bracket = None
+    opening_bracket: Optional[Leaf] = None
+    closing_bracket: Optional[Leaf] = None
     for leaf in reversed(line.leaves):
         if current_leaves is body_leaves:
             if leaf is opening_bracket:
@@ -2239,28 +4611,18 @@ def right_hand_split(  # noqa C901
                 opening_bracket = leaf.opening_bracket
                 closing_bracket = leaf
                 current_leaves = body_leaves
-    tail_leaves.reverse()
-    body_leaves.reverse()
-    head_leaves.reverse()
-    # Since body is a new indent level, remove spurious leading whitespace.
-    if body_leaves:
-        normalize_prefix(body_leaves[0], inside_brackets=True)
-    if not head_leaves:
-        # No `head` means the split failed. Either `tail` has all content or
+    if not (opening_bracket and closing_bracket and head_leaves):
+        # If there is no opening or closing_bracket that means the split failed and
+        # all content is in the tail.  Otherwise, if `head_leaves` are empty, it means
         # the matching `opening_bracket` wasn't available on `line` anymore.
         raise CannotSplit("No brackets found")
 
-    if line.is_import and len(body_leaves) == 1:
-        body_leaves.append(Leaf(token.COMMA, ","))
-
-    # Build the new lines.
-    for result, leaves in (head, head_leaves), (body, body_leaves), (tail, tail_leaves):
-        for leaf in leaves:
-            result.append(leaf, preformatted=True)
-            for comment_after in line.comments_after(leaf):
-                result.append(comment_after, preformatted=True)
-    assert opening_bracket and closing_bracket
-    body.should_explode = should_explode(body, opening_bracket)
+    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)
     bracket_split_succeeded_or_raise(head, body, tail)
     if (
         # the body shouldn't be exploded
@@ -2281,7 +4643,7 @@ def right_hand_split(  # noqa C901
     ):
         omit = {id(closing_bracket), *omit}
         try:
-            yield from right_hand_split(line, line_length, py36=py36, omit=omit)
+            yield from right_hand_split(line, line_length, features=features, omit=omit)
             return
 
         except CannotSplit:
@@ -2295,10 +4657,10 @@ def right_hand_split(  # noqa C901
 
             elif head.contains_multiline_strings() or 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 "
-                    "contains multiline strings which by definition never fit one "
-                    "line."
+                    "The current optional pair of parentheses is bound to fail to"
+                    " satisfy the splitting algorithm because the head or the tail"
+                    " contains multiline strings which by definition never fit one"
+                    " line."
                 )
 
     ensure_visible(opening_bracket)
@@ -2329,20 +4691,62 @@ def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None
 
         elif tail_len < 3:
             raise CannotSplit(
-                f"Splitting brackets on an empty body to save "
-                f"{tail_len} characters is not worth it"
+                f"Splitting brackets on an empty body to save {tail_len} characters is"
+                " not worth it"
+            )
+
+
+def bracket_split_build_line(
+    leaves: List[Leaf], original: Line, opening_bracket: Leaf, *, is_body: bool = False
+) -> 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.
+    """
+    result = Line(depth=original.depth)
+    if is_body:
+        result.inside_brackets = True
+        result.depth += 1
+        if leaves:
+            # Since body is a new indent level, remove spurious leading whitespace.
+            normalize_prefix(leaves[0], inside_brackets=True)
+            # Ensure a trailing comma for imports and standalone function arguments, but
+            # be careful not to add one after any comments or within type annotations.
+            no_commas = (
+                original.is_def
+                and opening_bracket.value == "("
+                and not any(l.type == token.COMMA for l in leaves)
             )
 
+            if original.is_import or no_commas:
+                for i in range(len(leaves) - 1, -1, -1):
+                    if leaves[i].type == STANDALONE_COMMENT:
+                        continue
+
+                    if leaves[i].type != token.COMMA:
+                        leaves.insert(i + 1, Leaf(token.COMMA, ","))
+                    break
+
+    # Populate the line
+    for leaf in leaves:
+        result.append(leaf, preformatted=True)
+        for comment_after in original.comments_after(leaf):
+            result.append(comment_after, preformatted=True)
+    if is_body:
+        result.should_explode = should_explode(result, opening_bracket)
+    return result
+
 
-def dont_increase_indentation(split_func: SplitFunc) -> SplitFunc:
+def dont_increase_indentation(split_func: Transformer) -> Transformer:
     """Normalize prefix of the first leaf in every line returned by `split_func`.
 
     This is a decorator over relevant split functions.
     """
 
     @wraps(split_func)
-    def split_wrapper(line: Line, py36: bool = False) -> Iterator[Line]:
-        for l in split_func(line, py36):
+    def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
+        for l in split_func(line, features):
             normalize_prefix(l.leaves[0], inside_brackets=True)
             yield l
 
@@ -2350,11 +4754,11 @@ def dont_increase_indentation(split_func: SplitFunc) -> SplitFunc:
 
 
 @dont_increase_indentation
-def delimiter_split(line: Line, py36: bool = False) -> Iterator[Line]:
+def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
     """Split according to delimiters of the highest priority.
 
-    If `py36` is True, the split will add trailing commas also in function
-    signatures that contain `*` and `**`.
+    If the appropriate Features are given, the split will add trailing commas
+    also in function signatures and calls that contain `*` and `**`.
     """
     try:
         last_leaf = line.leaves[-1]
@@ -2380,23 +4784,29 @@ def delimiter_split(line: Line, py36: bool = False) -> Iterator[Line]:
         nonlocal current_line
         try:
             current_line.append_safe(leaf, preformatted=True)
-        except ValueError as ve:
+        except ValueError:
             yield current_line
 
             current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
             current_line.append(leaf)
 
-    for index, leaf in enumerate(line.leaves):
+    for leaf in line.leaves:
         yield from append_to_line(leaf)
 
-        for comment_after in line.comments_after(leaf, index):
+        for comment_after in line.comments_after(leaf):
             yield from append_to_line(comment_after)
 
         lowest_depth = min(lowest_depth, leaf.bracket_depth)
-        if leaf.bracket_depth == lowest_depth and is_vararg(
-            leaf, within=VARARGS_PARENTS
-        ):
-            trailing_comma_safe = trailing_comma_safe and py36
+        if leaf.bracket_depth == lowest_depth:
+            if is_vararg(leaf, within={syms.typedargslist}):
+                trailing_comma_safe = (
+                    trailing_comma_safe and Feature.TRAILING_COMMA_IN_DEF in features
+                )
+            elif is_vararg(leaf, within={syms.arglist, syms.argument}):
+                trailing_comma_safe = (
+                    trailing_comma_safe and Feature.TRAILING_COMMA_IN_CALL in features
+                )
+
         leaf_priority = bt.delimiters.get(id(leaf))
         if leaf_priority == delimiter_priority:
             yield current_line
@@ -2414,7 +4824,9 @@ def delimiter_split(line: Line, py36: bool = False) -> Iterator[Line]:
 
 
 @dont_increase_indentation
-def standalone_comment_split(line: Line, py36: bool = False) -> Iterator[Line]:
+def standalone_comment_split(
+    line: Line, features: Collection[Feature] = ()
+) -> Iterator[Line]:
     """Split standalone comments from the rest of the line."""
     if not line.contains_standalone_comments(0):
         raise CannotSplit("Line does not have any standalone comments")
@@ -2426,16 +4838,16 @@ def standalone_comment_split(line: Line, py36: bool = False) -> Iterator[Line]:
         nonlocal current_line
         try:
             current_line.append_safe(leaf, preformatted=True)
-        except ValueError as ve:
+        except ValueError:
             yield current_line
 
             current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
             current_line.append(leaf)
 
-    for index, leaf in enumerate(line.leaves):
+    for leaf in line.leaves:
         yield from append_to_line(leaf)
 
-        for comment_after in line.comments_after(leaf, index):
+        for comment_after in line.comments_after(leaf):
             yield from append_to_line(comment_after)
 
     if current_line:
@@ -2456,6 +4868,14 @@ 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."""
+    t = leaf.type
+    v = leaf.value
+    return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix)
+
+
 def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
     """Leave existing extra newlines if not `inside_brackets`. Remove everything
     else.
@@ -2481,10 +4901,10 @@ def normalize_string_prefix(leaf: Leaf, remove_u_prefix: bool = False) -> None:
 
     Note: Mutates its argument.
     """
-    match = re.match(r"^([furbFURB]*)(.*)$", leaf.value, re.DOTALL)
+    match = re.match(r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", leaf.value, re.DOTALL)
     assert match is not None, f"failed to match string {leaf.value!r}"
     orig_prefix = match.group(1)
-    new_prefix = orig_prefix.lower()
+    new_prefix = orig_prefix.replace("F", "f").replace("B", "b").replace("U", "u")
     if remove_u_prefix:
         new_prefix = new_prefix.replace("u", "")
     leaf.value = f"{new_prefix}{match.group(2)}"
@@ -2498,7 +4918,7 @@ def normalize_string_quotes(leaf: Leaf) -> None:
 
     Note: Mutates its argument.
     """
-    value = leaf.value.lstrip("furbFURB")
+    value = leaf.value.lstrip(STRING_PREFIX_CHARS)
     if value[:3] == '"""':
         return
 
@@ -2538,11 +4958,20 @@ def normalize_string_quotes(leaf: Leaf) -> None:
         new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body)
         new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body)
     if "f" in prefix.casefold():
-        matches = re.findall(r"[^{]\{(.*?)\}[^}]", new_body)
+        matches = re.findall(
+            r"""
+            (?:[^{]|^)\{  # start of the string or a non-{ followed by a single {
+                ([^{].*?)  # contents of the brackets except if begins with {{
+            \}(?:[^}]|$)  # A } followed by end of the string or a non-}
+            """,
+            new_body,
+            re.VERBOSE,
+        )
         for m in matches:
             if "\\" in str(m):
                 # Do not introduce backslashes in interpolated expressions
                 return
+
     if new_quote == '"""' and new_body[-1:] == '"':
         # edge case:
         new_body = new_body[:-1] + '\\"'
@@ -2557,11 +4986,11 @@ def normalize_string_quotes(leaf: Leaf) -> None:
     leaf.value = f"{prefix}{new_quote}{new_body}{new_quote}"
 
 
-def normalize_numeric_literal(leaf: Leaf, allow_underscores: bool) -> None:
+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), and long number literals are split using underscores.
+    in Python 2 long literals).
     """
     text = leaf.value.lower()
     if text.startswith(("0o", "0b")):
@@ -2579,8 +5008,7 @@ def normalize_numeric_literal(leaf: Leaf, allow_underscores: bool) -> None:
             sign = "-"
         elif after.startswith("+"):
             after = after[1:]
-        before = format_float_or_int_string(before, allow_underscores)
-        after = format_int_string(after, allow_underscores)
+        before = format_float_or_int_string(before)
         text = f"{before}e{sign}{after}"
     elif text.endswith(("j", "l")):
         number = text[:-1]
@@ -2588,56 +5016,25 @@ def normalize_numeric_literal(leaf: Leaf, allow_underscores: bool) -> None:
         # Capitalize in "2L" because "l" looks too similar to "1".
         if suffix == "l":
             suffix = "L"
-        text = f"{format_float_or_int_string(number, allow_underscores)}{suffix}"
+        text = f"{format_float_or_int_string(number)}{suffix}"
     else:
-        text = format_float_or_int_string(text, allow_underscores)
+        text = format_float_or_int_string(text)
     leaf.value = text
 
 
-def format_float_or_int_string(text: str, allow_underscores: bool) -> str:
+def format_float_or_int_string(text: str) -> str:
     """Formats a float string like "1.0"."""
     if "." not in text:
-        return format_int_string(text, allow_underscores)
-
-    before, after = text.split(".")
-    before = format_int_string(before, allow_underscores) if before else "0"
-    if after:
-        after = format_int_string(after, allow_underscores, count_from_end=False)
-    else:
-        after = "0"
-    return f"{before}.{after}"
-
-
-def format_int_string(
-    text: str, allow_underscores: bool, count_from_end: bool = True
-) -> str:
-    """Normalizes underscores in a string to e.g. 1_000_000.
-
-    Input must be a string of digits and optional underscores.
-    If count_from_end is False, we add underscores after groups of three digits
-    counting from the beginning instead of the end of the strings. This is used
-    for the fractional part of float literals.
-    """
-    if not allow_underscores:
-        return text
-
-    text = text.replace("_", "")
-    if len(text) <= 5:
-        # No underscores for numbers <= 5 digits long.
         return text
 
-    if count_from_end:
-        # Avoid removing leading zeros, which are important if we're formatting
-        # part of a number like "0.001".
-        return format(int("1" + text), "3_")[1:].lstrip("_")
-    else:
-        return "_".join(text[i : i + 3] for i in range(0, len(text), 3))
+    before, after = text.split(".")
+    return f"{before or 0}.{after or 0}"
 
 
 def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
     """Make existing optional parentheses invisible or create new ones.
 
-    `parens_after` is a set of string leaf values immeditely after which parens
+    `parens_after` is a set of string leaf values immediately after which parens
     should be put.
 
     Standardizes on visible parentheses for single-element tuples, and keeps
@@ -2647,22 +5044,30 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
         if pc.value in FMT_OFF:
             # This `node` has a prefix with `# fmt: off`, don't mess with parens.
             return
-
     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)
+
+        # Add parentheses around long tuple unpacking in assignments.
+        if (
+            index == 0
+            and isinstance(child, Node)
+            and child.type == syms.testlist_star_expr
+        ):
+            check_lpar = True
+
         if check_lpar:
+            if is_walrus_assignment(child):
+                continue
+
             if child.type == syms.atom:
-                if maybe_make_parens_invisible_in_atom(child):
-                    lpar = Leaf(token.LPAR, "")
-                    rpar = Leaf(token.RPAR, "")
-                    index = child.remove() or 0
-                    node.insert_child(index, Node(syms.atom, [lpar, child, rpar]))
+                if maybe_make_parens_invisible_in_atom(child, parent=node):
+                    wrap_in_parentheses(node, child, visible=False)
             elif is_one_tuple(child):
-                # wrap child in visible parentheses
-                lpar = Leaf(token.LPAR, "(")
-                rpar = Leaf(token.RPAR, ")")
-                child.remove()
-                node.insert_child(index, Node(syms.atom, [lpar, child, rpar]))
+                wrap_in_parentheses(node, child, visible=True)
             elif node.type == syms.import_from:
                 # "import from" nodes store parentheses directly as part of
                 # the statement
@@ -2677,11 +5082,7 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
                 break
 
             elif not (isinstance(child, Leaf) and is_multiline_string(child)):
-                # wrap child in invisible parentheses
-                lpar = Leaf(token.LPAR, "")
-                rpar = Leaf(token.RPAR, "")
-                index = child.remove() or 0
-                node.insert_child(index, Node(syms.atom, [lpar, child, rpar]))
+                wrap_in_parentheses(node, child, visible=False)
 
         check_lpar = isinstance(child, Leaf) and child.value in parens_after
 
@@ -2725,7 +5126,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
                     # That happens when one of the `ignored_nodes` ended with a NEWLINE
                     # leaf (possibly followed by a DEDENT).
                     hidden_value = hidden_value[:-1]
-                first_idx = None
+                first_idx: Optional[int] = None
                 for ignored in ignored_nodes:
                     index = ignored.remove()
                     if first_idx is None:
@@ -2754,17 +5155,55 @@ def generate_ignored_nodes(leaf: Leaf) -> Iterator[LN]:
     """
     container: Optional[LN] = container_of(leaf)
     while container is not None and container.type != token.ENDMARKER:
-        for comment in list_comments(container.prefix, is_endmarker=False):
-            if comment.value in FMT_ON:
-                return
+        if 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):
+                    return
+                yield child
+        else:
+            yield container
+            container = container.next_sibling
+
+
+def fmt_on(container: LN) -> bool:
+    is_fmt_on = False
+    for comment in list_comments(container.prefix, is_endmarker=False):
+        if comment.value in FMT_ON:
+            is_fmt_on = True
+        elif comment.value in FMT_OFF:
+            is_fmt_on = False
+    return is_fmt_on
+
+
+def contains_fmt_on_at_column(container: LN, column: int) -> bool:
+    for child in container.children:
+        if (
+            isinstance(child, Node)
+            and first_leaf_column(child) == column
+            or isinstance(child, Leaf)
+            and child.column == column
+        ):
+            if fmt_on(child):
+                return True
+
+    return False
 
-        yield container
 
-        container = container.next_sibling
+def first_leaf_column(node: Node) -> Optional[int]:
+    for child in node.children:
+        if isinstance(child, Leaf):
+            return child.column
+    return None
 
 
-def maybe_make_parens_invisible_in_atom(node: LN) -> bool:
+def maybe_make_parens_invisible_in_atom(node: LN, parent: LN) -> 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.
 
@@ -2773,7 +5212,7 @@ def maybe_make_parens_invisible_in_atom(node: LN) -> bool:
         node.type != syms.atom
         or is_empty_tuple(node)
         or is_one_tuple(node)
-        or is_yield(node)
+        or (is_yield(node) and parent.type != syms.expr_stmt)
         or max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY
     ):
         return False
@@ -2781,16 +5220,40 @@ def maybe_make_parens_invisible_in_atom(node: LN) -> bool:
     first = node.children[0]
     last = node.children[-1]
     if first.type == token.LPAR and last.type == token.RPAR:
+        middle = node.children[1]
         # make parentheses invisible
         first.value = ""  # type: ignore
         last.value = ""  # type: ignore
-        if len(node.children) > 1:
-            maybe_make_parens_invisible_in_atom(node.children[1])
+        maybe_make_parens_invisible_in_atom(middle, parent=parent)
+
+        if is_atom_with_invisible_parens(middle):
+            # Strip the invisible parens from `middle` by replacing
+            # it with the child in-between the invisible parens
+            middle.replace(middle.children[1])
+
         return False
 
     return True
 
 
+def is_atom_with_invisible_parens(node: LN) -> bool:
+    """Given a `LN`, determines whether it's an atom `node` with invisible
+    parens. Useful in dedupe-ing and normalizing parens.
+    """
+    if isinstance(node, Leaf) or node.type != syms.atom:
+        return False
+
+    first, last = node.children[0], node.children[-1]
+    return (
+        isinstance(first, Leaf)
+        and first.type == token.LPAR
+        and first.value == ""
+        and isinstance(last, Leaf)
+        and last.type == token.RPAR
+        and last.value == ""
+    )
+
+
 def is_empty_tuple(node: LN) -> bool:
     """Return True if `node` holds an empty tuple."""
     return (
@@ -2801,18 +5264,43 @@ def is_empty_tuple(node: LN) -> bool:
     )
 
 
+def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]:
+    """Returns `wrapped` if `node` is of the shape ( wrapped ).
+
+    Parenthesis can be optional. Returns None otherwise"""
+    if len(node.children) != 3:
+        return None
+
+    lpar, wrapped, rpar = node.children
+    if not (lpar.type == token.LPAR and rpar.type == token.RPAR):
+        return None
+
+    return wrapped
+
+
+def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None:
+    """Wrap `child` in parentheses.
+
+    This replaces `child` with an atom holding the parentheses and the old
+    child.  That requires moving the prefix.
+
+    If `visible` is False, the leaves will be valueless (and thus invisible).
+    """
+    lpar = Leaf(token.LPAR, "(" if visible else "")
+    rpar = Leaf(token.RPAR, ")" if visible else "")
+    prefix = child.prefix
+    child.prefix = ""
+    index = child.remove() or 0
+    new_child = Node(syms.atom, [lpar, child, rpar])
+    new_child.prefix = prefix
+    parent.insert_child(index, new_child)
+
+
 def is_one_tuple(node: LN) -> bool:
     """Return True if `node` holds a tuple with one element, with or without parens."""
     if node.type == syms.atom:
-        if len(node.children) != 3:
-            return False
-
-        lpar, gexp, rpar = node.children
-        if not (
-            lpar.type == token.LPAR
-            and gexp.type == syms.testlist_gexp
-            and rpar.type == token.RPAR
-        ):
+        gexp = unwrap_singleton_parenthesis(node)
+        if gexp is None or gexp.type != syms.testlist_gexp:
             return False
 
         return len(gexp.children) == 2 and gexp.children[1].type == token.COMMA
@@ -2824,6 +5312,12 @@ def is_one_tuple(node: LN) -> bool:
     )
 
 
+def is_walrus_assignment(node: LN) -> bool:
+    """Return True iff `node` is of the shape ( test := test )"""
+    inner = unwrap_singleton_parenthesis(node)
+    return inner is not None and inner.type == syms.namedexpr_test
+
+
 def is_yield(node: LN) -> bool:
     """Return True if `node` holds a `yield` or `yield from` expression."""
     if node.type == syms.yield_expr:
@@ -2853,7 +5347,7 @@ def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool:
     extended iterable unpacking (PEP 3132) and additional unpacking
     generalizations (PEP 448).
     """
-    if leaf.type not in STARS or not leaf.parent:
+    if leaf.type not in VARARGS_SPECIALS or not leaf.parent:
         return False
 
     p = leaf.parent
@@ -2870,8 +5364,7 @@ def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool:
 
 def is_multiline_string(leaf: Leaf) -> bool:
     """Return True if `leaf` is a multiline string that actually spans many lines."""
-    value = leaf.value.lstrip("furbFURB")
-    return value[:3] in {'"""', "'''"} and "\n" in value
+    return has_triple_quotes(leaf.value) and "\n" in leaf.value
 
 
 def is_stub_suite(node: Node) -> bool:
@@ -2903,7 +5396,7 @@ def is_stub_body(node: LN) -> bool:
     )
 
 
-def max_delimiter_priority_in_atom(node: LN) -> int:
+def max_delimiter_priority_in_atom(node: LN) -> Priority:
     """Return maximum delimiter priority inside `node`.
 
     This is specific to atoms with contents contained in a pair of parentheses.
@@ -2935,7 +5428,7 @@ def ensure_visible(leaf: Leaf) -> None:
     """Make sure parentheses are visible.
 
     They could be invisible as part of some statements (see
-    :func:`normalize_invible_parens` and :func:`visit_import_from`).
+    :func:`normalize_invisible_parens` and :func:`visit_import_from`).
     """
     if leaf.type == token.LPAR:
         leaf.value = "("
@@ -2945,6 +5438,7 @@ def ensure_visible(leaf: Leaf) -> None:
 
 def should_explode(line: Line, opening_bracket: Leaf) -> bool:
     """Should `line` immediately be split with `delimiter_split()` after RHS?"""
+
     if not (
         opening_bracket.parent
         and opening_bracket.parent.type in {syms.atom, syms.import_from}
@@ -2962,39 +5456,61 @@ def should_explode(line: Line, opening_bracket: Leaf) -> bool:
     return max_priority == COMMA_PRIORITY
 
 
-def is_python36(node: Node) -> bool:
-    """Return True if the current file is using Python 3.6+ features.
+def get_features_used(node: Node) -> Set[Feature]:
+    """Return a set of (relatively) new Python features used in this file.
 
     Currently looking for:
     - f-strings;
-    - underscores in numeric literals; and
-    - trailing commas after * or ** in function signatures and calls.
+    - underscores in numeric literals;
+    - trailing commas after * or ** in function signatures and calls;
+    - positional only arguments in function signatures and lambdas;
     """
+    features: Set[Feature] = set()
     for n in node.pre_order():
         if n.type == token.STRING:
             value_head = n.value[:2]  # type: ignore
             if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
-                return True
+                features.add(Feature.F_STRINGS)
 
         elif n.type == token.NUMBER:
             if "_" in n.value:  # type: ignore
-                return True
+                features.add(Feature.NUMERIC_UNDERSCORES)
+
+        elif n.type == token.SLASH:
+            if n.parent and n.parent.type in {syms.typedargslist, syms.arglist}:
+                features.add(Feature.POS_ONLY_ARGUMENTS)
+
+        elif n.type == token.COLONEQUAL:
+            features.add(Feature.ASSIGNMENT_EXPRESSIONS)
 
         elif (
             n.type in {syms.typedargslist, syms.arglist}
             and n.children
             and n.children[-1].type == token.COMMA
         ):
+            if n.type == syms.typedargslist:
+                feature = Feature.TRAILING_COMMA_IN_DEF
+            else:
+                feature = Feature.TRAILING_COMMA_IN_CALL
+
             for ch in n.children:
                 if ch.type in STARS:
-                    return True
+                    features.add(feature)
 
                 if ch.type == syms.argument:
                     for argch in ch.children:
                         if argch.type in STARS:
-                            return True
+                            features.add(feature)
 
-    return False
+    return features
+
+
+def detect_target_versions(node: Node) -> Set[TargetVersion]:
+    """Detect the version to target based on the nodes used."""
+    features = get_features_used(node)
+    return {
+        version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
+    }
 
 
 def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[LeafID]]:
@@ -3011,8 +5527,8 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf
     yield omit
 
     length = 4 * line.depth
-    opening_bracket = None
-    closing_bracket = None
+    opening_bracket: Optional[Leaf] = None
+    closing_bracket: Optional[Leaf] = None
     inner_brackets: Set[LeafID] = set()
     for index, leaf, leaf_length in enumerate_with_length(line, reversed=True):
         length += leaf_length
@@ -3056,19 +5572,23 @@ def get_future_imports(node: Node) -> Set[str]:
             if isinstance(child, Leaf):
                 if child.type == token.NAME:
                     yield child.value
+
             elif child.type == syms.import_as_name:
                 orig_name = child.children[0]
                 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports"
                 assert orig_name.type == token.NAME, "Invalid syntax parsing imports"
                 yield orig_name.value
+
             elif child.type == syms.import_as_names:
                 yield from get_imports_from_children(child.children)
+
             else:
-                assert False, "Invalid syntax parsing imports"
+                raise AssertionError("Invalid syntax parsing imports")
 
     for child in node.children:
         if child.type != syms.simple_stmt:
             break
+
         first_child = child.children[0]
         if isinstance(first_child, Leaf):
             # Continue looking if we see a docstring; otherwise stop.
@@ -3078,24 +5598,39 @@ def get_future_imports(node: Node) -> Set[str]:
                 and child.children[1].type == token.NEWLINE
             ):
                 continue
-            else:
-                break
+
+            break
+
         elif first_child.type == syms.import_from:
             module_name = first_child.children[1]
             if not isinstance(module_name, Leaf) or module_name.value != "__future__":
                 break
+
             imports |= set(get_imports_from_children(first_child.children[3:]))
         else:
             break
+
     return imports
 
 
+@lru_cache()
+def get_gitignore(root: Path) -> PathSpec:
+    """ Return a PathSpec matching gitignore content if present."""
+    gitignore = root / ".gitignore"
+    lines: List[str] = []
+    if gitignore.is_file():
+        with gitignore.open() as gf:
+            lines = gf.readlines()
+    return PathSpec.from_lines("gitwildmatch", lines)
+
+
 def gen_python_files_in_dir(
     path: Path,
     root: Path,
     include: Pattern[str],
     exclude: Pattern[str],
     report: "Report",
+    gitignore: PathSpec,
 ) -> Iterator[Path]:
     """Generate all files under `path` whose paths are not excluded by the
     `exclude` regex, but are included by the `include` regex.
@@ -3106,8 +5641,18 @@ def gen_python_files_in_dir(
     """
     assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
     for child in path.iterdir():
+        # First ignore files matching .gitignore
+        if gitignore.match_file(child.as_posix()):
+            report.path_ignored(child, "matches the .gitignore file content")
+            continue
+
+        # Then ignore with `exclude` option.
         try:
             normalized_path = "/" + child.resolve().relative_to(root).as_posix()
+        except OSError as e:
+            report.path_ignored(child, f"cannot be read because {e}")
+            continue
+
         except ValueError:
             if child.is_symlink():
                 report.path_ignored(
@@ -3119,13 +5664,16 @@ def gen_python_files_in_dir(
 
         if child.is_dir():
             normalized_path += "/"
+
         exclude_match = exclude.search(normalized_path)
         if exclude_match and exclude_match.group(0):
-            report.path_ignored(child, f"matches the --exclude regular expression")
+            report.path_ignored(child, "matches the --exclude regular expression")
             continue
 
         if child.is_dir():
-            yield from gen_python_files_in_dir(child, root, include, exclude, report)
+            yield from gen_python_files_in_dir(
+                child, root, include, exclude, report, gitignore
+            )
 
         elif child.is_file():
             include_match = include.search(normalized_path)
@@ -3151,7 +5699,7 @@ def find_project_root(srcs: Iterable[str]) -> Path:
         # Append a fake file so `parents` below returns `common_base_dir`, too.
         common_base /= "fake-file"
     for directory in common_base.parents:
-        if (directory / ".git").is_dir():
+        if (directory / ".git").exists():
             return directory
 
         if (directory / ".hg").is_dir():
@@ -3168,6 +5716,7 @@ class Report:
     """Provides a reformatting counter. Can be rendered with `str(report)`."""
 
     check: bool = False
+    diff: bool = False
     quiet: bool = False
     verbose: bool = False
     change_count: int = 0
@@ -3177,7 +5726,7 @@ class Report:
     def done(self, src: Path, changed: Changed) -> None:
         """Increment the counter for successful reformatting. Write out a message."""
         if changed is Changed.YES:
-            reformatted = "would reformat" if self.check else "reformatted"
+            reformatted = "would reformat" if self.check or self.diff else "reformatted"
             if self.verbose or not self.quiet:
                 out(f"{reformatted} {src}")
             self.change_count += 1
@@ -3223,7 +5772,7 @@ class Report:
 
         Use `click.unstyle` to remove colors.
         """
-        if self.check:
+        if self.check or self.diff:
             reformatted = "would be reformatted"
             unchanged = "would be left unchanged"
             failed = "would fail to reformat"
@@ -3248,91 +5797,148 @@ class Report:
         return ", ".join(report) + "."
 
 
-def assert_equivalent(src: str, dst: str) -> None:
-    """Raise AssertionError if `src` and `dst` aren't equivalent."""
+def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]:
+    filename = "<unknown>"
+    if sys.version_info >= (3, 8):
+        # TODO: support Python 4+ ;)
+        for minor_version in range(sys.version_info[1], 4, -1):
+            try:
+                return ast.parse(src, filename, feature_version=(3, minor_version))
+            except SyntaxError:
+                continue
+    else:
+        for feature_version in (7, 6):
+            try:
+                return ast3.parse(src, filename, feature_version=feature_version)
+            except SyntaxError:
+                continue
 
-    import ast
-    import traceback
+    return ast27.parse(src)
 
-    def _v(node: ast.AST, depth: int = 0) -> Iterator[str]:
-        """Simple visitor generating strings to compare ASTs by content."""
-        yield f"{'  ' * depth}{node.__class__.__name__}("
 
-        for field in sorted(node._fields):
-            try:
-                value = getattr(node, field)
-            except AttributeError:
-                continue
+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
 
-            yield f"{'  ' * (depth+1)}{field}="
 
-            if isinstance(value, list):
-                for item in value:
-                    if isinstance(item, ast.AST):
-                        yield from _v(item, depth + 2)
+def _stringify_ast(
+    node: Union[ast.AST, ast3.AST, ast27.AST], depth: int = 0
+) -> Iterator[str]:
+    """Simple visitor generating strings to compare ASTs by content."""
 
-            elif isinstance(value, ast.AST):
-                yield from _v(value, depth + 2)
+    node = _fixup_ast_constants(node)
 
+    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):
+            break
+
+        try:
+            value = getattr(node, field)
+        except AttributeError:
+            continue
+
+        yield f"{'  ' * (depth+1)}{field}="
+
+        if isinstance(value, list):
+            for item in value:
+                # Ignore nested tuples within del statements, because we may insert
+                # 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))
+                ):
+                    for item in item.elts:
+                        yield from _stringify_ast(item, depth + 2)
+
+                elif isinstance(item, (ast.AST, ast3.AST, ast27.AST)):
+                    yield from _stringify_ast(item, depth + 2)
+
+        elif isinstance(value, (ast.AST, ast3.AST, ast27.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
+            if (
+                isinstance(node, ast.Constant)
+                and field == "value"
+                and isinstance(value, str)
+            ):
+                normalized = re.sub(r"\n[ \t]+", "\n ", value)
             else:
-                yield f"{'  ' * (depth+2)}{value!r},  # {value.__class__.__name__}"
+                normalized = value
+            yield f"{'  ' * (depth+2)}{normalized!r},  # {value.__class__.__name__}"
 
-        yield f"{'  ' * depth})  # /{node.__class__.__name__}"
+    yield f"{'  ' * depth})  # /{node.__class__.__name__}"
 
+
+def assert_equivalent(src: str, dst: str) -> None:
+    """Raise AssertionError if `src` and `dst` aren't equivalent."""
     try:
-        src_ast = ast.parse(src)
+        src_ast = parse_ast(src)
     except Exception as exc:
-        major, minor = sys.version_info[:2]
         raise AssertionError(
-            f"cannot use --safe with this file; failed to parse source file "
-            f"with Python {major}.{minor}'s builtin AST. Re-run with --fast "
-            f"or stop using deprecated Python 2 syntax. AST error message: {exc}"
+            "cannot use --safe with this file; failed to parse source file.  AST"
+            f" error message: {exc}"
         )
 
     try:
-        dst_ast = ast.parse(dst)
+        dst_ast = parse_ast(dst)
     except Exception as exc:
         log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
         raise AssertionError(
-            f"INTERNAL ERROR: Black produced invalid code: {exc}. "
-            f"Please report a bug on https://github.com/ambv/black/issues.  "
-            f"This invalid output might be helpful: {log}"
+            f"INTERNAL ERROR: Black produced invalid code: {exc}. Please report a bug"
+            " on https://github.com/psf/black/issues.  This invalid output might be"
+            f" helpful: {log}"
         ) from None
 
-    src_ast_str = "\n".join(_v(src_ast))
-    dst_ast_str = "\n".join(_v(dst_ast))
+    src_ast_str = "\n".join(_stringify_ast(src_ast))
+    dst_ast_str = "\n".join(_stringify_ast(dst_ast))
     if src_ast_str != dst_ast_str:
         log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
         raise AssertionError(
-            f"INTERNAL ERROR: Black produced code that is not equivalent to "
-            f"the source.  "
-            f"Please report a bug on https://github.com/ambv/black/issues.  "
-            f"This diff might be helpful: {log}"
+            "INTERNAL ERROR: Black produced code that is not equivalent to the"
+            " source.  Please report a bug on https://github.com/psf/black/issues. "
+            f" This diff might be helpful: {log}"
         ) from None
 
 
-def assert_stable(
-    src: str, dst: str, line_length: int, mode: FileMode = FileMode.AUTO_DETECT
-) -> None:
+def assert_stable(src: str, dst: str, mode: Mode) -> None:
     """Raise AssertionError if `dst` reformats differently the second time."""
-    newdst = format_str(dst, line_length=line_length, mode=mode)
+    newdst = format_str(dst, mode=mode)
     if dst != newdst:
         log = dump_to_file(
             diff(src, dst, "source", "first pass"),
             diff(dst, newdst, "first pass", "second pass"),
         )
         raise AssertionError(
-            f"INTERNAL ERROR: Black produced different code on the second pass "
-            f"of the formatter.  "
-            f"Please report a bug on https://github.com/ambv/black/issues.  "
-            f"This diff might be helpful: {log}"
+            "INTERNAL ERROR: Black produced different code on the second pass of the"
+            " formatter.  Please report a bug on https://github.com/psf/black/issues."
+            f"  This diff might be helpful: {log}"
         ) from None
 
 
+@mypyc_attr(patchable=True)
 def dump_to_file(*output: str) -> str:
     """Dump `output` to a temporary file. Return path to the file."""
-    import tempfile
-
     with tempfile.NamedTemporaryFile(
         mode="w", prefix="blk_", suffix=".log", delete=False, encoding="utf8"
     ) as f:
@@ -3343,29 +5949,42 @@ def dump_to_file(*output: str) -> str:
     return f.name
 
 
+@contextmanager
+def nullcontext() -> Iterator[None]:
+    """Return an empty context manager.
+
+    To be used like `nullcontext` in Python 3.7.
+    """
+    yield
+
+
 def diff(a: str, b: str, a_name: str, b_name: str) -> str:
     """Return a unified diff string between strings `a` and `b`."""
     import difflib
 
-    a_lines = [line + "\n" for line in a.split("\n")]
-    b_lines = [line + "\n" for line in b.split("\n")]
+    a_lines = [line + "\n" for line in a.splitlines()]
+    b_lines = [line + "\n" for line in b.splitlines()]
     return "".join(
         difflib.unified_diff(a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5)
     )
 
 
-def cancel(tasks: Iterable[asyncio.Task]) -> None:
+def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None:
     """asyncio signal handler that cancels all `tasks` and reports to stderr."""
     err("Aborted!")
     for task in tasks:
         task.cancel()
 
 
-def shutdown(loop: BaseEventLoop) -> None:
+def shutdown(loop: asyncio.AbstractEventLoop) -> None:
     """Cancel all pending tasks on `loop`, wait for them, and close the loop."""
     try:
+        if sys.version_info[:2] >= (3, 7):
+            all_tasks = asyncio.all_tasks
+        else:
+            all_tasks = asyncio.Task.all_tasks
         # This part is borrowed from asyncio/runners.py in Python 3.7b2.
-        to_cancel = [task for task in asyncio.Task.all_tasks(loop) if not task.done()]
+        to_cancel = [task for task in all_tasks(loop) if not task.done()]
         if not to_cancel:
             return
 
@@ -3399,7 +6018,8 @@ def re_compile_maybe_verbose(regex: str) -> Pattern[str]:
     """
     if "\n" in regex:
         regex = "(?x)" + regex
-    return re.compile(regex)
+    compiled: Pattern[str] = re.compile(regex)
+    return compiled
 
 
 def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]:
@@ -3426,8 +6046,7 @@ def enumerate_with_length(
         if "\n" in leaf.value:
             return  # Multiline strings, we can't continue.
 
-        comment: Optional[Leaf]
-        for comment in line.comments_after(leaf, index):
+        for comment in line.comments_after(leaf):
             length += len(comment.value)
 
         yield index, leaf, length
@@ -3439,7 +6058,7 @@ def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") ->
     Uses the provided `line_str` rendering, if any, otherwise computes a new one.
     """
     if not line_str:
-        line_str = str(line).strip("\n")
+        line_str = line_to_string(line)
     return (
         len(line_str) <= line_length
         and "\n" not in line_str  # multiline strings
@@ -3572,23 +6191,23 @@ def can_omit_invisible_parens(line: Line, line_length: int) -> bool:
     return False
 
 
-def get_cache_file(line_length: int, mode: FileMode) -> Path:
-    return CACHE_DIR / f"cache.{line_length}.{mode.value}.pickle"
+def get_cache_file(mode: Mode) -> Path:
+    return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle"
 
 
-def read_cache(line_length: int, mode: FileMode) -> Cache:
+def read_cache(mode: Mode) -> Cache:
     """Read the cache if it exists and is well formed.
 
     If it is not well formed, the call to write_cache later should resolve the issue.
     """
-    cache_file = get_cache_file(line_length, mode)
+    cache_file = get_cache_file(mode)
     if not cache_file.exists():
         return {}
 
     with cache_file.open("rb") as fobj:
         try:
             cache: Cache = pickle.load(fobj)
-        except pickle.UnpicklingError:
+        except (pickle.UnpicklingError, ValueError):
             return {}
 
     return cache
@@ -3616,17 +6235,15 @@ def filter_cached(cache: Cache, sources: Iterable[Path]) -> Tuple[Set[Path], Set
     return todo, done
 
 
-def write_cache(
-    cache: Cache, sources: Iterable[Path], line_length: int, mode: FileMode
-) -> None:
+def write_cache(cache: Cache, sources: Iterable[Path], mode: Mode) -> None:
     """Update the cache file."""
-    cache_file = get_cache_file(line_length, mode)
+    cache_file = get_cache_file(mode)
     try:
-        if not CACHE_DIR.exists():
-            CACHE_DIR.mkdir(parents=True)
+        CACHE_DIR.mkdir(parents=True, exist_ok=True)
         new_cache = {**cache, **{src.resolve(): get_cache_info(src) for src in sources}}
-        with cache_file.open("wb") as fobj:
-            pickle.dump(new_cache, fobj, protocol=pickle.HIGHEST_PROTOCOL)
+        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
 
@@ -3653,6 +6270,38 @@ def patch_click() -> None:
             module._verify_python3_env = lambda: None
 
 
-if __name__ == "__main__":
+def patched_main() -> None:
+    freeze_support()
     patch_click()
     main()
+
+
+def fix_docstring(docstring: str, prefix: str) -> str:
+    # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
+    if not docstring:
+        return ""
+    # Convert tabs to spaces (following the normal Python rules)
+    # and split into a list of lines:
+    lines = docstring.expandtabs().splitlines()
+    # Determine minimum indentation (first line doesn't count):
+    indent = sys.maxsize
+    for line in lines[1:]:
+        stripped = line.lstrip()
+        if stripped:
+            indent = min(indent, len(line) - len(stripped))
+    # Remove indentation (first line is special):
+    trimmed = [lines[0].strip()]
+    if indent < sys.maxsize:
+        last_line_idx = len(lines) - 2
+        for i, line in enumerate(lines[1:]):
+            stripped_line = line[indent:].rstrip()
+            if stripped_line or i == last_line_idx:
+                trimmed.append(prefix + stripped_line)
+            else:
+                trimmed.append("")
+    # Return a single string:
+    return "\n".join(trimmed)
+
+
+if __name__ == "__main__":
+    patched_main()