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

madduck's git repository

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

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

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

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

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

Add --skip-magic-trailing-comma (#1824)
authorShantanu <12621235+hauntsaninja@users.noreply.github.com>
Mon, 18 Jan 2021 00:59:06 +0000 (16:59 -0800)
committerGitHub <noreply@github.com>
Mon, 18 Jan 2021 00:59:06 +0000 (16:59 -0800)
README.md
docs/blackd.md
docs/installation_and_usage.md
docs/the_black_code_style.md
src/black/__init__.py
src/blackd/__init__.py
tests/data/expression_skip_magic_trailing_comma.diff [new file with mode: 0644]
tests/test_black.py

index 269bf5aaa43260d7f91fe7eb75d5beba370cd829..f1ec76938f01fc4ee7932958af04714b76cc57b6 100644 (file)
--- a/README.md
+++ b/README.md
@@ -97,6 +97,10 @@ Options:
 
   -S, --skip-string-normalization
                                   Don't normalize string quotes or prefixes.
 
   -S, --skip-string-normalization
                                   Don't normalize string quotes or prefixes.
+  -C, --skip-magic-trailing-comma
+                                  Don't use trailing commas as a reason to
+                                  split lines.
+
   --check                         Don't write the files back, just return the
                                   status.  Return code 0 means nothing would
                                   change.  Return code 1 means some files
   --check                         Don't write the files back, just return the
                                   status.  Return code 0 means nothing would
                                   change.  Return code 1 means some files
@@ -127,18 +131,19 @@ Options:
                                   paths are excluded. Use forward slashes for
                                   directories on all platforms (Windows, too).
                                   Exclusions are calculated first, inclusions
                                   paths are excluded. Use forward slashes for
                                   directories on all platforms (Windows, too).
                                   Exclusions are calculated first, inclusions
-                                  later.  [default: /(\.eggs|\.git|\.hg|\.mypy
-                                  _cache|\.nox|\.tox|\.venv|\.svn|_build|buck-
-                                  out|build|dist)/]
+                                  later.  [default: /(\.direnv|\.eggs|\.git|\.
+                                  hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu
+                                  ild|buck-out|build|dist)/]
 
   --force-exclude TEXT            Like --exclude, but files and directories
                                   matching this regex will be excluded even
 
   --force-exclude TEXT            Like --exclude, but files and directories
                                   matching this regex will be excluded even
-                                  when they are passed explicitly as arguments.
+                                  when they are passed explicitly as
+                                  arguments.
 
   --stdin-filename TEXT           The name of the file when passing it through
 
   --stdin-filename TEXT           The name of the file when passing it through
-                                  stdin. Useful to make sure Black will respect
-                                  --force-exclude option on some editors that
-                                  rely on using stdin.
+                                  stdin. Useful to make sure Black will
+                                  respect --force-exclude option on some
+                                  editors that rely on using stdin.
 
   -q, --quiet                     Don't emit non-error messages to stderr.
                                   Errors are still emitted; silence those with
 
   -q, --quiet                     Don't emit non-error messages to stderr.
                                   Errors are still emitted; silence those with
index c341308e1e447c1bc0ecd4c9db6288584a90da30..c8058ee7c63ed2e567f0dc51e72cf14e27314598 100644 (file)
@@ -54,6 +54,9 @@ The headers controlling how source code is formatted are:
 - `X-Skip-String-Normalization`: corresponds to the `--skip-string-normalization`
   command line flag. If present and its value is not the empty string, no string
   normalization will be performed.
 - `X-Skip-String-Normalization`: corresponds to the `--skip-string-normalization`
   command line flag. If present and its value is not the empty string, no string
   normalization will be performed.
+- `X-Skip-Magic-Trailing-Comma`: corresponds to the `--skip-magic-trailing-comma`
+  command line flag. If present and its value is not the empty string, trailing commas
+  will not be used as a reason to split lines.
 - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the
   `--fast` command line flag.
 - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the
 - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as _Black_ does when passed the
   `--fast` command line flag.
 - `X-Python-Variant`: if set to `pyi`, `blackd` will act as _Black_ does when passed the
index d0dd0c99dc5158dcaba4b37f914f20f02fa98575..e3b53fd076ead2324fbe37395a8a8d11ae3a0603 100644 (file)
@@ -52,6 +52,10 @@ Options:
 
   -S, --skip-string-normalization
                                   Don't normalize string quotes or prefixes.
 
   -S, --skip-string-normalization
                                   Don't normalize string quotes or prefixes.
+  -C, --skip-magic-trailing-comma
+                                  Don't use trailing commas as a reason to
+                                  split lines.
+
   --check                         Don't write the files back, just return the
                                   status.  Return code 0 means nothing would
                                   change.  Return code 1 means some files
   --check                         Don't write the files back, just return the
                                   status.  Return code 0 means nothing would
                                   change.  Return code 1 means some files
@@ -82,13 +86,19 @@ Options:
                                   paths are excluded. Use forward slashes for
                                   directories on all platforms (Windows, too).
                                   Exclusions are calculated first, inclusions
                                   paths are excluded. Use forward slashes for
                                   directories on all platforms (Windows, too).
                                   Exclusions are calculated first, inclusions
-                                  later.  [default: /(\.eggs|\.git|\.hg|\.mypy
-                                  _cache|\.nox|\.tox|\.venv|\.svn|_build|buck-
-                                  out|build|dist)/]
+                                  later.  [default: /(\.direnv|\.eggs|\.git|\.
+                                  hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu
+                                  ild|buck-out|build|dist)/]
 
   --force-exclude TEXT            Like --exclude, but files and directories
                                   matching this regex will be excluded even
 
   --force-exclude TEXT            Like --exclude, but files and directories
                                   matching this regex will be excluded even
-                                  when they are passed explicitly as arguments.
+                                  when they are passed explicitly as
+                                  arguments.
+
+  --stdin-filename TEXT           The name of the file when passing it through
+                                  stdin. Useful to make sure Black will
+                                  respect --force-exclude option on some
+                                  editors that rely on using stdin.
 
   --stdin-filename TEXT           The name of the file when passing it through
                                   stdin. Useful to make sure Black will respect
 
   --stdin-filename TEXT           The name of the file when passing it through
                                   stdin. Useful to make sure Black will respect
index 19464ba482a083e6d46d3f4db09ce2ec3d4a8c6d..a4e55c1744c9645219fa2fe359766692cf805e41 100644 (file)
@@ -438,6 +438,9 @@ into one item per line.
 How do you make it stop? Just delete that trailing comma and _Black_ will collapse your
 collection into one line if it fits.
 
 How do you make it stop? Just delete that trailing comma and _Black_ will collapse your
 collection into one line if it fits.
 
+If you must, you can recover the behaviour of early versions of Black with the option
+`--skip-magic-trailing-comma` / `-C`.
+
 ### r"strings" and R"strings"
 
 _Black_ normalizes string quotes as well as string prefixes, making them lowercase. One
 ### r"strings" and R"strings"
 
 _Black_ normalizes string quotes as well as string prefixes, making them lowercase. One
index 91f70d96165748741583ce2d7b94007b71111f87..9034bf6cf77226880e7e2fc05fd8f95a3a647573 100644 (file)
@@ -260,6 +260,7 @@ class Mode:
     target_versions: Set[TargetVersion] = field(default_factory=set)
     line_length: int = DEFAULT_LINE_LENGTH
     string_normalization: bool = True
     target_versions: Set[TargetVersion] = field(default_factory=set)
     line_length: int = DEFAULT_LINE_LENGTH
     string_normalization: bool = True
+    magic_trailing_comma: bool = True
     experimental_string_processing: bool = False
     is_pyi: bool = False
 
     experimental_string_processing: bool = False
     is_pyi: bool = False
 
@@ -397,6 +398,12 @@ def target_version_option_callback(
     is_flag=True,
     help="Don't normalize string quotes or prefixes.",
 )
     is_flag=True,
     help="Don't normalize string quotes or prefixes.",
 )
+@click.option(
+    "-C",
+    "--skip-magic-trailing-comma",
+    is_flag=True,
+    help="Don't use trailing commas as a reason to split lines.",
+)
 @click.option(
     "--experimental-string-processing",
     is_flag=True,
 @click.option(
     "--experimental-string-processing",
     is_flag=True,
@@ -524,6 +531,7 @@ def main(
     fast: bool,
     pyi: bool,
     skip_string_normalization: bool,
     fast: bool,
     pyi: bool,
     skip_string_normalization: bool,
+    skip_magic_trailing_comma: bool,
     experimental_string_processing: bool,
     quiet: bool,
     verbose: bool,
     experimental_string_processing: bool,
     quiet: bool,
     verbose: bool,
@@ -546,6 +554,7 @@ def main(
         line_length=line_length,
         is_pyi=pyi,
         string_normalization=not skip_string_normalization,
         line_length=line_length,
         is_pyi=pyi,
         string_normalization=not skip_string_normalization,
+        magic_trailing_comma=not skip_magic_trailing_comma,
         experimental_string_processing=experimental_string_processing,
     )
     if config and verbose:
         experimental_string_processing=experimental_string_processing,
     )
     if config and verbose:
@@ -1022,13 +1031,12 @@ def format_str(src_contents: str, *, mode: Mode) -> FileContent:
         versions = detect_target_versions(src_node)
     normalize_fmt_off(src_node)
     lines = LineGenerator(
         versions = detect_target_versions(src_node)
     normalize_fmt_off(src_node)
     lines = LineGenerator(
+        mode=mode,
         remove_u_prefix="unicode_literals" in future_imports
         or supports_feature(versions, Feature.UNICODE_LITERALS),
         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=mode.is_pyi)
     )
     elt = EmptyLineTracker(is_pyi=mode.is_pyi)
-    empty_line = Line()
+    empty_line = Line(mode=mode)
     after = 0
     split_line_features = {
         feature
     after = 0
     split_line_features = {
         feature
@@ -1464,6 +1472,7 @@ class BracketTracker:
 class Line:
     """Holds leaves and comments. Can be printed with `str(line)`."""
 
 class Line:
     """Holds leaves and comments. Can be printed with `str(line)`."""
 
+    mode: Mode
     depth: int = 0
     leaves: List[Leaf] = field(default_factory=list)
     # keys ordered like `leaves`
     depth: int = 0
     leaves: List[Leaf] = field(default_factory=list)
     # keys ordered like `leaves`
@@ -1496,8 +1505,11 @@ class Line:
             )
         if self.inside_brackets or not preformatted:
             self.bracket_tracker.mark(leaf)
             )
         if self.inside_brackets or not preformatted:
             self.bracket_tracker.mark(leaf)
-            if self.maybe_should_explode(leaf):
-                self.should_explode = True
+            if self.mode.magic_trailing_comma:
+                if self.has_magic_trailing_comma(leaf):
+                    self.should_explode = True
+            elif self.has_magic_trailing_comma(leaf, ensure_removable=True):
+                self.remove_trailing_comma()
         if not self.append_comment(leaf):
             self.leaves.append(leaf)
 
         if not self.append_comment(leaf):
             self.leaves.append(leaf)
 
@@ -1673,10 +1685,14 @@ class Line:
     def contains_multiline_strings(self) -> bool:
         return any(is_multiline_string(leaf) for leaf in self.leaves)
 
     def contains_multiline_strings(self) -> bool:
         return any(is_multiline_string(leaf) for leaf in self.leaves)
 
-    def maybe_should_explode(self, closing: Leaf) -> bool:
-        """Return True if this line should explode (always be split), that is when:
-        - there's a trailing comma here; and
-        - it's not a one-tuple.
+    def has_magic_trailing_comma(
+        self, closing: Leaf, ensure_removable: bool = False
+    ) -> bool:
+        """Return True if we have a magic trailing comma, that is when:
+        - there's a trailing comma here
+        - it's not a one-tuple
+        Additionally, if ensure_removable:
+        - it's not from square bracket indexing
         """
         if not (
             closing.type in CLOSING_BRACKETS
         """
         if not (
             closing.type in CLOSING_BRACKETS
@@ -1685,9 +1701,15 @@ class Line:
         ):
             return False
 
         ):
             return False
 
-        if closing.type in {token.RBRACE, token.RSQB}:
+        if closing.type == token.RBRACE:
             return True
 
             return True
 
+        if closing.type == token.RSQB:
+            if not ensure_removable:
+                return True
+            comma = self.leaves[-1]
+            return bool(comma.parent and comma.parent.type == syms.listmaker)
+
         if self.is_import:
             return True
 
         if self.is_import:
             return True
 
@@ -1765,6 +1787,7 @@ class Line:
 
     def clone(self) -> "Line":
         return Line(
 
     def clone(self) -> "Line":
         return Line(
+            mode=self.mode,
             depth=self.depth,
             inside_brackets=self.inside_brackets,
             should_explode=self.should_explode,
             depth=self.depth,
             inside_brackets=self.inside_brackets,
             should_explode=self.should_explode,
@@ -1923,10 +1946,9 @@ class LineGenerator(Visitor[Line]):
     in ways that will no longer stringify to valid Python code on the tree.
     """
 
     in ways that will no longer stringify to valid Python code on the tree.
     """
 
-    is_pyi: bool = False
-    normalize_strings: bool = True
-    current_line: Line = field(default_factory=Line)
+    mode: Mode
     remove_u_prefix: bool = False
     remove_u_prefix: bool = False
+    current_line: Line = field(init=False)
 
     def line(self, indent: int = 0) -> Iterator[Line]:
         """Generate a line.
 
     def line(self, indent: int = 0) -> Iterator[Line]:
         """Generate a line.
@@ -1941,7 +1963,7 @@ class LineGenerator(Visitor[Line]):
             return  # Line is empty, don't emit. Creating a new one unnecessary.
 
         complete_line = self.current_line
             return  # Line is empty, don't emit. Creating a new one unnecessary.
 
         complete_line = self.current_line
-        self.current_line = Line(depth=complete_line.depth + indent)
+        self.current_line = Line(mode=self.mode, depth=complete_line.depth + indent)
         yield complete_line
 
     def visit_default(self, node: LN) -> Iterator[Line]:
         yield complete_line
 
     def visit_default(self, node: LN) -> Iterator[Line]:
@@ -1965,7 +1987,7 @@ class LineGenerator(Visitor[Line]):
                     yield from self.line()
 
             normalize_prefix(node, inside_brackets=any_open_brackets)
                     yield from self.line()
 
             normalize_prefix(node, inside_brackets=any_open_brackets)
-            if self.normalize_strings and node.type == token.STRING:
+            if self.mode.string_normalization and node.type == token.STRING:
                 normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
                 normalize_string_quotes(node)
             if node.type == token.NUMBER:
                 normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
                 normalize_string_quotes(node)
             if node.type == token.NUMBER:
@@ -2017,7 +2039,7 @@ class LineGenerator(Visitor[Line]):
 
     def visit_suite(self, node: Node) -> Iterator[Line]:
         """Visit a suite."""
 
     def visit_suite(self, node: Node) -> Iterator[Line]:
         """Visit a suite."""
-        if self.is_pyi and is_stub_suite(node):
+        if self.mode.is_pyi and is_stub_suite(node):
             yield from self.visit(node.children[2])
         else:
             yield from self.visit_default(node)
             yield from self.visit(node.children[2])
         else:
             yield from self.visit_default(node)
@@ -2026,7 +2048,7 @@ class LineGenerator(Visitor[Line]):
         """Visit a statement without nested statements."""
         is_suite_like = node.parent and node.parent.type in STATEMENT
         if is_suite_like:
         """Visit a statement without nested statements."""
         is_suite_like = node.parent and node.parent.type in STATEMENT
         if is_suite_like:
-            if self.is_pyi and is_stub_body(node):
+            if self.mode.is_pyi and is_stub_body(node):
                 yield from self.visit_default(node)
             else:
                 yield from self.line(+1)
                 yield from self.visit_default(node)
             else:
                 yield from self.line(+1)
@@ -2034,7 +2056,11 @@ class LineGenerator(Visitor[Line]):
                 yield from self.line(-1)
 
         else:
                 yield from self.line(-1)
 
         else:
-            if not self.is_pyi or not node.parent or not is_stub_suite(node.parent):
+            if (
+                not self.mode.is_pyi
+                or not node.parent
+                or not is_stub_suite(node.parent)
+            ):
                 yield from self.line()
             yield from self.visit_default(node)
 
                 yield from self.line()
             yield from self.visit_default(node)
 
@@ -2110,6 +2136,8 @@ class LineGenerator(Visitor[Line]):
 
     def __post_init__(self) -> None:
         """You are in a twisty little maze of passages."""
 
     def __post_init__(self) -> None:
         """You are in a twisty little maze of passages."""
+        self.current_line = Line(mode=self.mode)
+
         v = self.visit_stmt
         Ø: Set[str] = set()
         self.visit_assert_stmt = partial(v, keywords={"assert"}, parens={"assert", ","})
         v = self.visit_stmt
         Ø: Set[str] = set()
         self.visit_assert_stmt = partial(v, keywords={"assert"}, parens={"assert", ","})
@@ -4350,6 +4378,7 @@ class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
         # `StringSplitter` will break it down further if necessary.
         string_value = LL[string_idx].value
         string_line = Line(
         # `StringSplitter` will break it down further if necessary.
         string_value = LL[string_idx].value
         string_line = Line(
+            mode=line.mode,
             depth=line.depth + 1,
             inside_brackets=True,
             should_explode=line.should_explode,
             depth=line.depth + 1,
             inside_brackets=True,
             should_explode=line.should_explode,
@@ -4943,7 +4972,7 @@ def bracket_split_build_line(
     If `is_body` is True, the result line is one-indented inside brackets and as such
     has its first leaf's prefix normalized and a trailing comma added when expected.
     """
     If `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)
+    result = Line(mode=original.mode, depth=original.depth)
     if is_body:
         result.inside_brackets = True
         result.depth += 1
     if is_body:
         result.inside_brackets = True
         result.depth += 1
@@ -5015,7 +5044,9 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[
         if bt.delimiter_count_with_priority(delimiter_priority) == 1:
             raise CannotSplit("Splitting a single attribute from its owner looks wrong")
 
         if bt.delimiter_count_with_priority(delimiter_priority) == 1:
             raise CannotSplit("Splitting a single attribute from its owner looks wrong")
 
-    current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
+    current_line = Line(
+        mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
+    )
     lowest_depth = sys.maxsize
     trailing_comma_safe = True
 
     lowest_depth = sys.maxsize
     trailing_comma_safe = True
 
@@ -5027,7 +5058,9 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[
         except ValueError:
             yield current_line
 
         except ValueError:
             yield current_line
 
-            current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
+            current_line = Line(
+                mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
+            )
             current_line.append(leaf)
 
     for leaf in line.leaves:
             current_line.append(leaf)
 
     for leaf in line.leaves:
@@ -5051,7 +5084,9 @@ def delimiter_split(line: Line, features: Collection[Feature] = ()) -> Iterator[
         if leaf_priority == delimiter_priority:
             yield current_line
 
         if leaf_priority == delimiter_priority:
             yield current_line
 
-            current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
+            current_line = Line(
+                mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
+            )
     if current_line:
         if (
             trailing_comma_safe
     if current_line:
         if (
             trailing_comma_safe
@@ -5072,7 +5107,9 @@ def standalone_comment_split(
     if not line.contains_standalone_comments(0):
         raise CannotSplit("Line does not have any standalone comments")
 
     if not line.contains_standalone_comments(0):
         raise CannotSplit("Line does not have any standalone comments")
 
-    current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
+    current_line = Line(
+        mode=line.mode, depth=line.depth, inside_brackets=line.inside_brackets
+    )
 
     def append_to_line(leaf: Leaf) -> Iterator[Line]:
         """Append `leaf` to current line or to new line if appending impossible."""
 
     def append_to_line(leaf: Leaf) -> Iterator[Line]:
         """Append `leaf` to current line or to new line if appending impossible."""
@@ -5082,7 +5119,9 @@ def standalone_comment_split(
         except ValueError:
             yield current_line
 
         except ValueError:
             yield current_line
 
-            current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
+            current_line = Line(
+                line.mode, depth=line.depth, inside_brackets=line.inside_brackets
+            )
             current_line.append(leaf)
 
     for leaf in line.leaves:
             current_line.append(leaf)
 
     for leaf in line.leaves:
@@ -5767,7 +5806,7 @@ def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool:
         return False
 
     return max_priority == COMMA_PRIORITY and (
         return False
 
     return max_priority == COMMA_PRIORITY and (
-        trailing_comma
+        (line.mode.magic_trailing_comma and trailing_comma)
         # always explode imports
         or opening_bracket.parent.type in {syms.atom, syms.import_from}
     )
         # always explode imports
         or opening_bracket.parent.type in {syms.atom, syms.import_from}
     )
index f77a5e8e7be49c0a5b881e22fe4401b5a36f221e..fc684730e4b8a4dde8eb8cfabbe444999da235b0 100644 (file)
@@ -32,6 +32,7 @@ PROTOCOL_VERSION_HEADER = "X-Protocol-Version"
 LINE_LENGTH_HEADER = "X-Line-Length"
 PYTHON_VARIANT_HEADER = "X-Python-Variant"
 SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
 LINE_LENGTH_HEADER = "X-Line-Length"
 PYTHON_VARIANT_HEADER = "X-Python-Variant"
 SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
+SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma"
 FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
 DIFF_HEADER = "X-Diff"
 
 FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
 DIFF_HEADER = "X-Diff"
 
@@ -40,6 +41,7 @@ BLACK_HEADERS = [
     LINE_LENGTH_HEADER,
     PYTHON_VARIANT_HEADER,
     SKIP_STRING_NORMALIZATION_HEADER,
     LINE_LENGTH_HEADER,
     PYTHON_VARIANT_HEADER,
     SKIP_STRING_NORMALIZATION_HEADER,
+    SKIP_MAGIC_TRAILING_COMMA,
     FAST_OR_SAFE_HEADER,
     DIFF_HEADER,
 ]
     FAST_OR_SAFE_HEADER,
     DIFF_HEADER,
 ]
@@ -114,6 +116,9 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
         skip_string_normalization = bool(
             request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False)
         )
         skip_string_normalization = bool(
             request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False)
         )
+        skip_magic_trailing_comma = bool(
+            request.headers.get(SKIP_MAGIC_TRAILING_COMMA, False)
+        )
         fast = False
         if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
             fast = True
         fast = False
         if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
             fast = True
@@ -122,6 +127,7 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
             is_pyi=pyi,
             line_length=line_length,
             string_normalization=not skip_string_normalization,
             is_pyi=pyi,
             line_length=line_length,
             string_normalization=not skip_string_normalization,
+            magic_trailing_comma=not skip_magic_trailing_comma,
         )
         req_bytes = await request.content.read()
         charset = request.charset if request.charset is not None else "utf8"
         )
         req_bytes = await request.content.read()
         charset = request.charset if request.charset is not None else "utf8"
diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/expression_skip_magic_trailing_comma.diff
new file mode 100644 (file)
index 0000000..8a0225b
--- /dev/null
@@ -0,0 +1,404 @@
+--- [Deterministic header]
++++ [Deterministic header]
+@@ -1,8 +1,8 @@
+ ...
+-'some_string'
+-b'\\xa3'
++"some_string"
++b"\\xa3"
+ Name
+ None
+ True
+ False
+ 1
+@@ -29,63 +29,84 @@
+ ~great
+ +value
+ -1
+ ~int and not v1 ^ 123 + v2 | True
+ (~int) and (not ((v1 ^ (123 + v2)) | True))
+-+really ** -confusing ** ~operator ** -precedence
+-flags & ~ select.EPOLLIN and waiters.write_task is not None
+++(really ** -(confusing ** ~(operator ** -precedence)))
++flags & ~select.EPOLLIN and waiters.write_task is not None
+ lambda arg: None
+ lambda a=True: a
+ lambda a, b, c=True: a
+-lambda a, b, c=True, *, d=(1 << v2), e='str': a
+-lambda a, b, c=True, *vararg, d=(v1 << 2), e='str', **kwargs: a + b
++lambda a, b, c=True, *, d=(1 << v2), e="str": a
++lambda a, b, c=True, *vararg, d=(v1 << 2), e="str", **kwargs: a + b
+ manylambdas = lambda x=lambda y=lambda z=1: z: y(): x()
+-foo = (lambda port_id, ignore_missing: {"port1": port1_resource, "port2": port2_resource}[port_id])
++foo = lambda port_id, ignore_missing: {
++    "port1": port1_resource,
++    "port2": port2_resource,
++}[port_id]
+ 1 if True else 2
+ str or None if True else str or bytes or None
+ (str or None) if True else (str or bytes or None)
+ str or None if (1 if True else 2) else str or bytes or None
+ (str or None) if (1 if True else 2) else (str or bytes or None)
+-((super_long_variable_name or None) if (1 if super_long_test_name else 2) else (str or bytes or None))
+-{'2.7': dead, '3.7': (long_live or die_hard)}
+-{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}}
++(
++    (super_long_variable_name or None)
++    if (1 if super_long_test_name else 2)
++    else (str or bytes or None)
++)
++{"2.7": dead, "3.7": (long_live or die_hard)}
++{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}}
+ {**a, **b, **c}
+-{'2.7', '3.6', '3.7', '3.8', '3.9', ('4.0' if gilectomy else '3.10')}
+-({'a': 'b'}, (True or False), (+value), 'string', b'bytes') or None
++{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")}
++({"a": "b"}, (True or False), (+value), "string", b"bytes") or None
+ ()
+ (1,)
+ (1, 2)
+ (1, 2, 3)
+ []
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)]
+-[1, 2, 3,]
++[1, 2, 3]
+ [*a]
+ [*range(10)]
+-[*a, 4, 5,]
+-[4, *a, 5,]
+-[this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, *more]
++[*a, 4, 5]
++[4, *a, 5]
++[
++    this_is_a_very_long_variable_which_will_force_a_delimiter_split,
++    element,
++    another,
++    *more,
++]
+ {i for i in (1, 2, 3)}
+ {(i ** 2) for i in (1, 2, 3)}
+-{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}
++{(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
+ {((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)}
+ [i for i in (1, 2, 3)]
+ [(i ** 2) for i in (1, 2, 3)]
+-[(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))]
++[(i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))]
+ [((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3)]
+ {i: 0 for i in (1, 2, 3)}
+-{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}
++{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))}
+ {a: b * 2 for a, b in dictionary.items()}
+ {a: b * -2 for a, b in dictionary.items()}
+-{k: v for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension}
++{
++    k: v
++    for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension
++}
+ Python3 > Python2 > COBOL
+ Life is Life
+ call()
+ call(arg)
+-call(kwarg='hey')
+-call(arg, kwarg='hey')
+-call(arg, another, kwarg='hey', **kwargs)
+-call(this_is_a_very_long_variable_which_will_force_a_delimiter_split, arg, another, kwarg='hey', **kwargs)  # note: no trailing comma pre-3.6
++call(kwarg="hey")
++call(arg, kwarg="hey")
++call(arg, another, kwarg="hey", **kwargs)
++call(
++    this_is_a_very_long_variable_which_will_force_a_delimiter_split,
++    arg,
++    another,
++    kwarg="hey",
++    **kwargs
++)  # note: no trailing comma pre-3.6
+ call(*gidgets[:2])
+ call(a, *gidgets[:2])
+ call(**self.screen_kwargs)
+ call(b, **self.screen_kwargs)
+ lukasz.langa.pl
+@@ -94,26 +115,24 @@
+ 1.0 .real
+ ....__class__
+ list[str]
+ dict[str, int]
+ tuple[str, ...]
+-tuple[
+-    str, int, float, dict[str, int]
+-]
++tuple[str, int, float, dict[str, int]]
+ tuple[str, int, float, dict[str, int],]
+ very_long_variable_name_filters: t.List[
+     t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]],
+ ]
+ xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod(  # type: ignore
+     sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
+ )
+ xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod(  # type: ignore
+     sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
+ )
+-xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[
+-    ..., List[SomeClass]
+-] = classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__))  # type: ignore
++xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod(
++    sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
++)  # type: ignore
+ slice[0]
+ slice[0:1]
+ slice[0:1:2]
+ slice[:]
+ slice[:-1]
+@@ -137,113 +156,178 @@
+ numpy[-(c + 1) :, d]
+ numpy[:, l[-2]]
+ numpy[:, ::-1]
+ numpy[np.newaxis, :]
+ (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None)
+-{'2.7': dead, '3.7': long_live or die_hard}
+-{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}
++{"2.7": dead, "3.7": long_live or die_hard}
++{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"}
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]
+ (SomeName)
+ SomeName
+ (Good, Bad, Ugly)
+ (i for i in (1, 2, 3))
+ ((i ** 2) for i in (1, 2, 3))
+-((i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c')))
++((i ** 2) for i, _ in ((1, "a"), (2, "b"), (3, "c")))
+ (((i ** 2) + j) for i in (1, 2, 3) for j in (1, 2, 3))
+ (*starred,)
+-{"id": "1","type": "type","started_at": now(),"ended_at": now() + timedelta(days=10),"priority": 1,"import_session_id": 1,**kwargs}
++{
++    "id": "1",
++    "type": "type",
++    "started_at": now(),
++    "ended_at": now() + timedelta(days=10),
++    "priority": 1,
++    "import_session_id": 1,
++    **kwargs,
++}
+ a = (1,)
+-b = 1,
++b = (1,)
+ c = 1
+ d = (1,) + a + (2,)
+ e = (1,).count(1)
+ f = 1, *range(10)
+ g = 1, *"ten"
+-what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(vars_to_remove)
+-what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(vars_to_remove)
+-result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc()).all()
+-result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc(),).all()
++what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(
++    vars_to_remove
++)
++what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(
++    vars_to_remove
++)
++result = (
++    session.query(models.Customer.id)
++    .filter(
++        models.Customer.account_id == account_id, models.Customer.email == email_address
++    )
++    .order_by(models.Customer.id.asc())
++    .all()
++)
++result = (
++    session.query(models.Customer.id)
++    .filter(
++        models.Customer.account_id == account_id, models.Customer.email == email_address
++    )
++    .order_by(models.Customer.id.asc())
++    .all()
++)
+ Ø = set()
+ authors.łukasz.say_thanks()
+ mapping = {
+     A: 0.25 * (10.0 / 12),
+     B: 0.1 * (10.0 / 12),
+     C: 0.1 * (10.0 / 12),
+     D: 0.1 * (10.0 / 12),
+ }
++
+ def gen():
+     yield from outside_of_generator
+-    a = (yield)
+-    b = ((yield))
+-    c = (((yield)))
++    a = yield
++    b = yield
++    c = yield
++
+ async def f():
+     await some.complicated[0].call(with_args=(True or (1 is not 1)))
+-print(* [] or [1])
++
++
++print(*[] or [1])
+ print(**{1: 3} if False else {x: x for x in range(3)})
+-print(* lambda x: x)
+-assert(not Test),("Short message")
+-assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message"
+-assert(((parens is TooMany)))
+-for x, in (1,), (2,), (3,): ...
+-for y in (): ...
+-for z in (i for i in (1, 2, 3)): ...
+-for i in (call()): ...
+-for j in (1 + (2 + 3)): ...
+-while(this and that): ...
+-for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'):
++print(*lambda x: x)
++assert not Test, "Short message"
++assert this is ComplexTest and not requirements.fit_in_a_single_line(
++    force=False
++), "Short message"
++assert parens is TooMany
++for (x,) in (1,), (2,), (3,):
++    ...
++for y in ():
++    ...
++for z in (i for i in (1, 2, 3)):
++    ...
++for i in call():
++    ...
++for j in 1 + (2 + 3):
++    ...
++while this and that:
++    ...
++for (
++    addr_family,
++    addr_type,
++    addr_proto,
++    addr_canonname,
++    addr_sockaddr,
++) in socket.getaddrinfo("google.com", "http"):
+     pass
+-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+-a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
+-if (
+-    threading.current_thread() != threading.main_thread() and
+-    threading.current_thread() != threading.main_thread() or
+-    signal.getsignal(signal.SIGINT) != signal.default_int_handler
+-):
+-    return True
+-if (
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-):
+-    return True
+-if (
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-):
+-    return True
+-if (
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-):
+-    return True
+-if (
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-):
+-    return True
+-if (
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa *
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-):
+-    return True
+-if (
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa /
+-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-):
+-    return True
+-if (
+-    ~ aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n
+-):
+-    return True
+-if (
+-    ~ aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n
+-):
+-    return True
+-if (
+-    ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
++a = (
++    aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp
++    in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
++)
++a = (
++    aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp
++    not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
++)
++a = (
++    aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp
++    is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
++)
++a = (
++    aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp
++    is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz
++)
++if (
++    threading.current_thread() != threading.main_thread()
++    and threading.current_thread() != threading.main_thread()
++    or signal.getsignal(signal.SIGINT) != signal.default_int_handler
++):
++    return True
++if (
++    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++):
++    return True
++if (
++    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++    & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++):
++    return True
++if (
++    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++    + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++):
++    return True
++if (
++    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++    - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++):
++    return True
++if (
++    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++    * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++):
++    return True
++if (
++    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++    / aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
++):
++    return True
++if (
++    ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e
++    | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l ** aaaa.m // aaaa.n
++):
++    return True
++if (
++    ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e
++    | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h
++    ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l ** aaaaaaaa.m // aaaaaaaa.n
++):
++    return True
++if (
++    ~aaaaaaaaaaaaaaaa.a
++    + aaaaaaaaaaaaaaaa.b
++    - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e
++    | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h
++    ^ aaaaaaaaaaaaaaaa.i
++    << aaaaaaaaaaaaaaaa.k
++    >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
+ ):
+     return True
+ last_call()
+ # standalone comment at ENDMARKER
index a688c8780efe682530d11ccf55c9995eea8608a8..28b75787bd6593d630c0812adfb2bbda340a6bc8 100644 (file)
@@ -395,6 +395,31 @@ class BlackTestCase(BlackBaseTestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, mode)
 
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, mode)
 
+    def test_skip_magic_trailing_comma(self) -> None:
+        source, _ = read_data("expression.py")
+        expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
+        tmp_file = Path(black.dump_to_file(source))
+        diff_header = re.compile(
+            rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
+            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+        )
+        try:
+            result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
+            self.assertEqual(result.exit_code, 0)
+        finally:
+            os.unlink(tmp_file)
+        actual = result.output
+        actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
+        actual = actual.rstrip() + "\n"  # the diff output has a trailing space
+        if expected != actual:
+            dump = black.dump_to_file(actual)
+            msg = (
+                "Expected diff isn't equal to the actual. If you made changes to"
+                " expression.py and this is an anticipated difference, overwrite"
+                f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
+            )
+            self.assertEqual(expected, actual, msg)
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_python2_print_function(self) -> None:
         source, expected = read_data("python2_print_function")
     @patch("black.dump_to_file", dump_to_stderr)
     def test_python2_print_function(self) -> None:
         source, expected = read_data("python2_print_function")