We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Fix AST safety check false negative
Fixes #4268

Previously we would allow whitespace changes in all strings, now
only in docstrings.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/black/parsing.py`
Content:
```
1 """
2 Parse Python code and perform AST validation.
3 """
4 
5 import ast
6 import sys
7 import warnings
8 from typing import Iterable, Iterator, List, Set, Tuple
9 
10 from black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature
11 from black.nodes import syms
12 from blib2to3 import pygram
13 from blib2to3.pgen2 import driver
14 from blib2to3.pgen2.grammar import Grammar
15 from blib2to3.pgen2.parse import ParseError
16 from blib2to3.pgen2.tokenize import TokenError
17 from blib2to3.pytree import Leaf, Node
18 
19 
20 class InvalidInput(ValueError):
21     """Raised when input source code fails all parse attempts."""
22 
23 
24 def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
25     if not target_versions:
26         # No target_version specified, so try all grammars.
27         return [
28             # Python 3.7-3.9
29             pygram.python_grammar_async_keywords,
30             # Python 3.0-3.6
31             pygram.python_grammar,
32             # Python 3.10+
33             pygram.python_grammar_soft_keywords,
34         ]
35 
36     grammars = []
37     # If we have to parse both, try to parse async as a keyword first
38     if not supports_feature(
39         target_versions, Feature.ASYNC_IDENTIFIERS
40     ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING):
41         # Python 3.7-3.9
42         grammars.append(pygram.python_grammar_async_keywords)
43     if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
44         # Python 3.0-3.6
45         grammars.append(pygram.python_grammar)
46     if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions):
47         # Python 3.10+
48         grammars.append(pygram.python_grammar_soft_keywords)
49 
50     # At least one of the above branches must have been taken, because every Python
51     # version has exactly one of the two 'ASYNC_*' flags
52     return grammars
53 
54 
55 def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node:
56     """Given a string with source, return the lib2to3 Node."""
57     if not src_txt.endswith("\n"):
58         src_txt += "\n"
59 
60     grammars = get_grammars(set(target_versions))
61     errors = {}
62     for grammar in grammars:
63         drv = driver.Driver(grammar)
64         try:
65             result = drv.parse_string(src_txt, True)
66             break
67 
68         except ParseError as pe:
69             lineno, column = pe.context[1]
70             lines = src_txt.splitlines()
71             try:
72                 faulty_line = lines[lineno - 1]
73             except IndexError:
74                 faulty_line = "<line number missing in source>"
75             errors[grammar.version] = InvalidInput(
76                 f"Cannot parse: {lineno}:{column}: {faulty_line}"
77             )
78 
79         except TokenError as te:
80             # In edge cases these are raised; and typically don't have a "faulty_line".
81             lineno, column = te.args[1]
82             errors[grammar.version] = InvalidInput(
83                 f"Cannot parse: {lineno}:{column}: {te.args[0]}"
84             )
85 
86     else:
87         # Choose the latest version when raising the actual parsing error.
88         assert len(errors) >= 1
89         exc = errors[max(errors)]
90         raise exc from None
91 
92     if isinstance(result, Leaf):
93         result = Node(syms.file_input, [result])
94     return result
95 
96 
97 def matches_grammar(src_txt: str, grammar: Grammar) -> bool:
98     drv = driver.Driver(grammar)
99     try:
100         drv.parse_string(src_txt, True)
101     except (ParseError, TokenError, IndentationError):
102         return False
103     else:
104         return True
105 
106 
107 def lib2to3_unparse(node: Node) -> str:
108     """Given a lib2to3 node, return its string representation."""
109     code = str(node)
110     return code
111 
112 
113 class ASTSafetyError(Exception):
114     """Raised when Black's generated code is not equivalent to the old AST."""
115 
116 
117 def _parse_single_version(
118     src: str, version: Tuple[int, int], *, type_comments: bool
119 ) -> ast.AST:
120     filename = "<unknown>"
121     with warnings.catch_warnings():
122         warnings.simplefilter("ignore", SyntaxWarning)
123         warnings.simplefilter("ignore", DeprecationWarning)
124         return ast.parse(
125             src, filename, feature_version=version, type_comments=type_comments
126         )
127 
128 
129 def parse_ast(src: str) -> ast.AST:
130     # TODO: support Python 4+ ;)
131     versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]
132 
133     first_error = ""
134     for version in sorted(versions, reverse=True):
135         try:
136             return _parse_single_version(src, version, type_comments=True)
137         except SyntaxError as e:
138             if not first_error:
139                 first_error = str(e)
140 
141     # Try to parse without type comments
142     for version in sorted(versions, reverse=True):
143         try:
144             return _parse_single_version(src, version, type_comments=False)
145         except SyntaxError:
146             pass
147 
148     raise SyntaxError(first_error)
149 
150 
151 def _normalize(lineend: str, value: str) -> str:
152     # To normalize, we strip any leading and trailing space from
153     # each line...
154     stripped: List[str] = [i.strip() for i in value.splitlines()]
155     normalized = lineend.join(stripped)
156     # ...and remove any blank lines at the beginning and end of
157     # the whole string
158     return normalized.strip()
159 
160 
161 def stringify_ast(node: ast.AST) -> Iterator[str]:
162     """Simple visitor generating strings to compare ASTs by content."""
163     return _stringify_ast(node, [])
164 
165 
166 def _stringify_ast_with_new_parent(
167     node: ast.AST, parent_stack: List[ast.AST], new_parent: ast.AST
168 ) -> Iterator[str]:
169     parent_stack.append(new_parent)
170     yield from _stringify_ast(node, parent_stack)
171     parent_stack.pop()
172 
173 
174 def _stringify_ast(node: ast.AST, parent_stack: List[ast.AST]) -> Iterator[str]:
175     if (
176         isinstance(node, ast.Constant)
177         and isinstance(node.value, str)
178         and node.kind == "u"
179     ):
180         # It's a quirk of history that we strip the u prefix over here. We used to
181         # rewrite the AST nodes for Python version compatibility and we never copied
182         # over the kind
183         node.kind = None
184 
185     yield f"{'    ' * len(parent_stack)}{node.__class__.__name__}("
186 
187     for field in sorted(node._fields):  # noqa: F402
188         # TypeIgnore has only one field 'lineno' which breaks this comparison
189         if isinstance(node, ast.TypeIgnore):
190             break
191 
192         try:
193             value: object = getattr(node, field)
194         except AttributeError:
195             continue
196 
197         yield f"{'    ' * (len(parent_stack) + 1)}{field}="
198 
199         if isinstance(value, list):
200             for item in value:
201                 # Ignore nested tuples within del statements, because we may insert
202                 # parentheses and they change the AST.
203                 if (
204                     field == "targets"
205                     and isinstance(node, ast.Delete)
206                     and isinstance(item, ast.Tuple)
207                 ):
208                     for elt in item.elts:
209                         yield from _stringify_ast_with_new_parent(
210                             elt, parent_stack, node
211                         )
212 
213                 elif isinstance(item, ast.AST):
214                     yield from _stringify_ast_with_new_parent(item, parent_stack, node)
215 
216         elif isinstance(value, ast.AST):
217             yield from _stringify_ast_with_new_parent(value, parent_stack, node)
218 
219         else:
220             normalized: object
221             if (
222                 isinstance(node, ast.Constant)
223                 and field == "value"
224                 and isinstance(value, str)
225                 and len(parent_stack) >= 2
226                 and isinstance(parent_stack[-1], ast.Expr)
227                 and isinstance(
228                     parent_stack[-2],
229                     (ast.FunctionDef, ast.AsyncFunctionDef, ast.Module, ast.ClassDef),
230                 )
231             ):
232                 # Constant strings may be indented across newlines, if they are
233                 # docstrings; fold spaces after newlines when comparing. Similarly,
234                 # trailing and leading space may be removed.
235                 normalized = _normalize("\n", value)
236             elif field == "type_comment" and isinstance(value, str):
237                 # Trailing whitespace in type comments is removed.
238                 normalized = value.rstrip()
239             else:
240                 normalized = value
241             yield (
242                 f"{'    ' * (len(parent_stack) + 1)}{normalized!r},  #"
243                 f" {value.__class__.__name__}"
244             )
245 
246     yield f"{'    ' * len(parent_stack)})  # /{node.__class__.__name__}"
247
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/black/parsing.py b/src/black/parsing.py
--- a/src/black/parsing.py
+++ b/src/black/parsing.py
@@ -223,11 +223,9 @@
                 and field == "value"
                 and isinstance(value, str)
                 and len(parent_stack) >= 2
+                # Any standalone string, ideally this would
+                # exactly match black.nodes.is_docstring
                 and isinstance(parent_stack[-1], ast.Expr)
-                and isinstance(
-                    parent_stack[-2],
-                    (ast.FunctionDef, ast.AsyncFunctionDef, ast.Module, ast.ClassDef),
-                )
             ):
                 # Constant strings may be indented across newlines, if they are
                 # docstrings; fold spaces after newlines when comparing. Similarly,

{"golden_diff": "diff --git a/src/black/parsing.py b/src/black/parsing.py\n--- a/src/black/parsing.py\n+++ b/src/black/parsing.py\n@@ -223,11 +223,9 @@\n                 and field == \"value\"\n                 and isinstance(value, str)\n                 and len(parent_stack) >= 2\n+                # Any standalone string, ideally this would\n+                # exactly match black.nodes.is_docstring\n                 and isinstance(parent_stack[-1], ast.Expr)\n-                and isinstance(\n-                    parent_stack[-2],\n-                    (ast.FunctionDef, ast.AsyncFunctionDef, ast.Module, ast.ClassDef),\n-                )\n             ):\n                 # Constant strings may be indented across newlines, if they are\n                 # docstrings; fold spaces after newlines when comparing. Similarly,\n", "issue": "Fix AST safety check false negative\nFixes #4268\r\n\r\nPreviously we would allow whitespace changes in all strings, now\r\nonly in docstrings.\r\n\n", "before_files": [{"content": "\"\"\"\nParse Python code and perform AST validation.\n\"\"\"\n\nimport ast\nimport sys\nimport warnings\nfrom typing import Iterable, Iterator, List, Set, Tuple\n\nfrom black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature\nfrom black.nodes import syms\nfrom blib2to3 import pygram\nfrom blib2to3.pgen2 import driver\nfrom blib2to3.pgen2.grammar import Grammar\nfrom blib2to3.pgen2.parse import ParseError\nfrom blib2to3.pgen2.tokenize import TokenError\nfrom blib2to3.pytree import Leaf, Node\n\n\nclass InvalidInput(ValueError):\n    \"\"\"Raised when input source code fails all parse attempts.\"\"\"\n\n\ndef get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:\n    if not target_versions:\n        # No target_version specified, so try all grammars.\n        return [\n            # Python 3.7-3.9\n            pygram.python_grammar_async_keywords,\n            # Python 3.0-3.6\n            pygram.python_grammar,\n            # Python 3.10+\n            pygram.python_grammar_soft_keywords,\n        ]\n\n    grammars = []\n    # If we have to parse both, try to parse async as a keyword first\n    if not supports_feature(\n        target_versions, Feature.ASYNC_IDENTIFIERS\n    ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING):\n        # Python 3.7-3.9\n        grammars.append(pygram.python_grammar_async_keywords)\n    if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):\n        # Python 3.0-3.6\n        grammars.append(pygram.python_grammar)\n    if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions):\n        # Python 3.10+\n        grammars.append(pygram.python_grammar_soft_keywords)\n\n    # At least one of the above branches must have been taken, because every Python\n    # version has exactly one of the two 'ASYNC_*' flags\n    return grammars\n\n\ndef lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node:\n    \"\"\"Given a string with source, return the lib2to3 Node.\"\"\"\n    if not src_txt.endswith(\"\\n\"):\n        src_txt += \"\\n\"\n\n    grammars = get_grammars(set(target_versions))\n    errors = {}\n    for grammar in grammars:\n        drv = driver.Driver(grammar)\n        try:\n            result = drv.parse_string(src_txt, True)\n            break\n\n        except ParseError as pe:\n            lineno, column = pe.context[1]\n            lines = src_txt.splitlines()\n            try:\n                faulty_line = lines[lineno - 1]\n            except IndexError:\n                faulty_line = \"<line number missing in source>\"\n            errors[grammar.version] = InvalidInput(\n                f\"Cannot parse: {lineno}:{column}: {faulty_line}\"\n            )\n\n        except TokenError as te:\n            # In edge cases these are raised; and typically don't have a \"faulty_line\".\n            lineno, column = te.args[1]\n            errors[grammar.version] = InvalidInput(\n                f\"Cannot parse: {lineno}:{column}: {te.args[0]}\"\n            )\n\n    else:\n        # Choose the latest version when raising the actual parsing error.\n        assert len(errors) >= 1\n        exc = errors[max(errors)]\n        raise exc from None\n\n    if isinstance(result, Leaf):\n        result = Node(syms.file_input, [result])\n    return result\n\n\ndef matches_grammar(src_txt: str, grammar: Grammar) -> bool:\n    drv = driver.Driver(grammar)\n    try:\n        drv.parse_string(src_txt, True)\n    except (ParseError, TokenError, IndentationError):\n        return False\n    else:\n        return True\n\n\ndef lib2to3_unparse(node: Node) -> str:\n    \"\"\"Given a lib2to3 node, return its string representation.\"\"\"\n    code = str(node)\n    return code\n\n\nclass ASTSafetyError(Exception):\n    \"\"\"Raised when Black's generated code is not equivalent to the old AST.\"\"\"\n\n\ndef _parse_single_version(\n    src: str, version: Tuple[int, int], *, type_comments: bool\n) -> ast.AST:\n    filename = \"<unknown>\"\n    with warnings.catch_warnings():\n        warnings.simplefilter(\"ignore\", SyntaxWarning)\n        warnings.simplefilter(\"ignore\", DeprecationWarning)\n        return ast.parse(\n            src, filename, feature_version=version, type_comments=type_comments\n        )\n\n\ndef parse_ast(src: str) -> ast.AST:\n    # TODO: support Python 4+ ;)\n    versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]\n\n    first_error = \"\"\n    for version in sorted(versions, reverse=True):\n        try:\n            return _parse_single_version(src, version, type_comments=True)\n        except SyntaxError as e:\n            if not first_error:\n                first_error = str(e)\n\n    # Try to parse without type comments\n    for version in sorted(versions, reverse=True):\n        try:\n            return _parse_single_version(src, version, type_comments=False)\n        except SyntaxError:\n            pass\n\n    raise SyntaxError(first_error)\n\n\ndef _normalize(lineend: str, value: str) -> str:\n    # To normalize, we strip any leading and trailing space from\n    # each line...\n    stripped: List[str] = [i.strip() for i in value.splitlines()]\n    normalized = lineend.join(stripped)\n    # ...and remove any blank lines at the beginning and end of\n    # the whole string\n    return normalized.strip()\n\n\ndef stringify_ast(node: ast.AST) -> Iterator[str]:\n    \"\"\"Simple visitor generating strings to compare ASTs by content.\"\"\"\n    return _stringify_ast(node, [])\n\n\ndef _stringify_ast_with_new_parent(\n    node: ast.AST, parent_stack: List[ast.AST], new_parent: ast.AST\n) -> Iterator[str]:\n    parent_stack.append(new_parent)\n    yield from _stringify_ast(node, parent_stack)\n    parent_stack.pop()\n\n\ndef _stringify_ast(node: ast.AST, parent_stack: List[ast.AST]) -> Iterator[str]:\n    if (\n        isinstance(node, ast.Constant)\n        and isinstance(node.value, str)\n        and node.kind == \"u\"\n    ):\n        # It's a quirk of history that we strip the u prefix over here. We used to\n        # rewrite the AST nodes for Python version compatibility and we never copied\n        # over the kind\n        node.kind = None\n\n    yield f\"{'    ' * len(parent_stack)}{node.__class__.__name__}(\"\n\n    for field in sorted(node._fields):  # noqa: F402\n        # TypeIgnore has only one field 'lineno' which breaks this comparison\n        if isinstance(node, ast.TypeIgnore):\n            break\n\n        try:\n            value: object = getattr(node, field)\n        except AttributeError:\n            continue\n\n        yield f\"{'    ' * (len(parent_stack) + 1)}{field}=\"\n\n        if isinstance(value, list):\n            for item in value:\n                # Ignore nested tuples within del statements, because we may insert\n                # parentheses and they change the AST.\n                if (\n                    field == \"targets\"\n                    and isinstance(node, ast.Delete)\n                    and isinstance(item, ast.Tuple)\n                ):\n                    for elt in item.elts:\n                        yield from _stringify_ast_with_new_parent(\n                            elt, parent_stack, node\n                        )\n\n                elif isinstance(item, ast.AST):\n                    yield from _stringify_ast_with_new_parent(item, parent_stack, node)\n\n        elif isinstance(value, ast.AST):\n            yield from _stringify_ast_with_new_parent(value, parent_stack, node)\n\n        else:\n            normalized: object\n            if (\n                isinstance(node, ast.Constant)\n                and field == \"value\"\n                and isinstance(value, str)\n                and len(parent_stack) >= 2\n                and isinstance(parent_stack[-1], ast.Expr)\n                and isinstance(\n                    parent_stack[-2],\n                    (ast.FunctionDef, ast.AsyncFunctionDef, ast.Module, ast.ClassDef),\n                )\n            ):\n                # Constant strings may be indented across newlines, if they are\n                # docstrings; fold spaces after newlines when comparing. Similarly,\n                # trailing and leading space may be removed.\n                normalized = _normalize(\"\\n\", value)\n            elif field == \"type_comment\" and isinstance(value, str):\n                # Trailing whitespace in type comments is removed.\n                normalized = value.rstrip()\n            else:\n                normalized = value\n            yield (\n                f\"{'    ' * (len(parent_stack) + 1)}{normalized!r},  #\"\n                f\" {value.__class__.__name__}\"\n            )\n\n    yield f\"{'    ' * len(parent_stack)})  # /{node.__class__.__name__}\"\n", "path": "src/black/parsing.py"}], "after_files": [{"content": "\"\"\"\nParse Python code and perform AST validation.\n\"\"\"\n\nimport ast\nimport sys\nimport warnings\nfrom typing import Iterable, Iterator, List, Set, Tuple\n\nfrom black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature\nfrom black.nodes import syms\nfrom blib2to3 import pygram\nfrom blib2to3.pgen2 import driver\nfrom blib2to3.pgen2.grammar import Grammar\nfrom blib2to3.pgen2.parse import ParseError\nfrom blib2to3.pgen2.tokenize import TokenError\nfrom blib2to3.pytree import Leaf, Node\n\n\nclass InvalidInput(ValueError):\n    \"\"\"Raised when input source code fails all parse attempts.\"\"\"\n\n\ndef get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:\n    if not target_versions:\n        # No target_version specified, so try all grammars.\n        return [\n            # Python 3.7-3.9\n            pygram.python_grammar_async_keywords,\n            # Python 3.0-3.6\n            pygram.python_grammar,\n            # Python 3.10+\n            pygram.python_grammar_soft_keywords,\n        ]\n\n    grammars = []\n    # If we have to parse both, try to parse async as a keyword first\n    if not supports_feature(\n        target_versions, Feature.ASYNC_IDENTIFIERS\n    ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING):\n        # Python 3.7-3.9\n        grammars.append(pygram.python_grammar_async_keywords)\n    if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):\n        # Python 3.0-3.6\n        grammars.append(pygram.python_grammar)\n    if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions):\n        # Python 3.10+\n        grammars.append(pygram.python_grammar_soft_keywords)\n\n    # At least one of the above branches must have been taken, because every Python\n    # version has exactly one of the two 'ASYNC_*' flags\n    return grammars\n\n\ndef lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node:\n    \"\"\"Given a string with source, return the lib2to3 Node.\"\"\"\n    if not src_txt.endswith(\"\\n\"):\n        src_txt += \"\\n\"\n\n    grammars = get_grammars(set(target_versions))\n    errors = {}\n    for grammar in grammars:\n        drv = driver.Driver(grammar)\n        try:\n            result = drv.parse_string(src_txt, True)\n            break\n\n        except ParseError as pe:\n            lineno, column = pe.context[1]\n            lines = src_txt.splitlines()\n            try:\n                faulty_line = lines[lineno - 1]\n            except IndexError:\n                faulty_line = \"<line number missing in source>\"\n            errors[grammar.version] = InvalidInput(\n                f\"Cannot parse: {lineno}:{column}: {faulty_line}\"\n            )\n\n        except TokenError as te:\n            # In edge cases these are raised; and typically don't have a \"faulty_line\".\n            lineno, column = te.args[1]\n            errors[grammar.version] = InvalidInput(\n                f\"Cannot parse: {lineno}:{column}: {te.args[0]}\"\n            )\n\n    else:\n        # Choose the latest version when raising the actual parsing error.\n        assert len(errors) >= 1\n        exc = errors[max(errors)]\n        raise exc from None\n\n    if isinstance(result, Leaf):\n        result = Node(syms.file_input, [result])\n    return result\n\n\ndef matches_grammar(src_txt: str, grammar: Grammar) -> bool:\n    drv = driver.Driver(grammar)\n    try:\n        drv.parse_string(src_txt, True)\n    except (ParseError, TokenError, IndentationError):\n        return False\n    else:\n        return True\n\n\ndef lib2to3_unparse(node: Node) -> str:\n    \"\"\"Given a lib2to3 node, return its string representation.\"\"\"\n    code = str(node)\n    return code\n\n\nclass ASTSafetyError(Exception):\n    \"\"\"Raised when Black's generated code is not equivalent to the old AST.\"\"\"\n\n\ndef _parse_single_version(\n    src: str, version: Tuple[int, int], *, type_comments: bool\n) -> ast.AST:\n    filename = \"<unknown>\"\n    with warnings.catch_warnings():\n        warnings.simplefilter(\"ignore\", SyntaxWarning)\n        warnings.simplefilter(\"ignore\", DeprecationWarning)\n        return ast.parse(\n            src, filename, feature_version=version, type_comments=type_comments\n        )\n\n\ndef parse_ast(src: str) -> ast.AST:\n    # TODO: support Python 4+ ;)\n    versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]\n\n    first_error = \"\"\n    for version in sorted(versions, reverse=True):\n        try:\n            return _parse_single_version(src, version, type_comments=True)\n        except SyntaxError as e:\n            if not first_error:\n                first_error = str(e)\n\n    # Try to parse without type comments\n    for version in sorted(versions, reverse=True):\n        try:\n            return _parse_single_version(src, version, type_comments=False)\n        except SyntaxError:\n            pass\n\n    raise SyntaxError(first_error)\n\n\ndef _normalize(lineend: str, value: str) -> str:\n    # To normalize, we strip any leading and trailing space from\n    # each line...\n    stripped: List[str] = [i.strip() for i in value.splitlines()]\n    normalized = lineend.join(stripped)\n    # ...and remove any blank lines at the beginning and end of\n    # the whole string\n    return normalized.strip()\n\n\ndef stringify_ast(node: ast.AST) -> Iterator[str]:\n    \"\"\"Simple visitor generating strings to compare ASTs by content.\"\"\"\n    return _stringify_ast(node, [])\n\n\ndef _stringify_ast_with_new_parent(\n    node: ast.AST, parent_stack: List[ast.AST], new_parent: ast.AST\n) -> Iterator[str]:\n    parent_stack.append(new_parent)\n    yield from _stringify_ast(node, parent_stack)\n    parent_stack.pop()\n\n\ndef _stringify_ast(node: ast.AST, parent_stack: List[ast.AST]) -> Iterator[str]:\n    if (\n        isinstance(node, ast.Constant)\n        and isinstance(node.value, str)\n        and node.kind == \"u\"\n    ):\n        # It's a quirk of history that we strip the u prefix over here. We used to\n        # rewrite the AST nodes for Python version compatibility and we never copied\n        # over the kind\n        node.kind = None\n\n    yield f\"{'    ' * len(parent_stack)}{node.__class__.__name__}(\"\n\n    for field in sorted(node._fields):  # noqa: F402\n        # TypeIgnore has only one field 'lineno' which breaks this comparison\n        if isinstance(node, ast.TypeIgnore):\n            break\n\n        try:\n            value: object = getattr(node, field)\n        except AttributeError:\n            continue\n\n        yield f\"{'    ' * (len(parent_stack) + 1)}{field}=\"\n\n        if isinstance(value, list):\n            for item in value:\n                # Ignore nested tuples within del statements, because we may insert\n                # parentheses and they change the AST.\n                if (\n                    field == \"targets\"\n                    and isinstance(node, ast.Delete)\n                    and isinstance(item, ast.Tuple)\n                ):\n                    for elt in item.elts:\n                        yield from _stringify_ast_with_new_parent(\n                            elt, parent_stack, node\n                        )\n\n                elif isinstance(item, ast.AST):\n                    yield from _stringify_ast_with_new_parent(item, parent_stack, node)\n\n        elif isinstance(value, ast.AST):\n            yield from _stringify_ast_with_new_parent(value, parent_stack, node)\n\n        else:\n            normalized: object\n            if (\n                isinstance(node, ast.Constant)\n                and field == \"value\"\n                and isinstance(value, str)\n                and len(parent_stack) >= 2\n                # Any standalone string, ideally this would\n                # exactly match black.nodes.is_docstring\n                and isinstance(parent_stack[-1], ast.Expr)\n            ):\n                # Constant strings may be indented across newlines, if they are\n                # docstrings; fold spaces after newlines when comparing. Similarly,\n                # trailing and leading space may be removed.\n                normalized = _normalize(\"\\n\", value)\n            elif field == \"type_comment\" and isinstance(value, str):\n                # Trailing whitespace in type comments is removed.\n                normalized = value.rstrip()\n            else:\n                normalized = value\n            yield (\n                f\"{'    ' * (len(parent_stack) + 1)}{normalized!r},  #\"\n                f\" {value.__class__.__name__}\"\n            )\n\n    yield f\"{'    ' * len(parent_stack)})  # /{node.__class__.__name__}\"\n", "path": "src/black/parsing.py"}]}