• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

psf / black / 17042588889

18 Aug 2025 01:51PM UTC coverage: 95.905%. Remained the same
17042588889

push

github

web-flow
Bump actions/checkout from 4 to 5 (#4735)

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

3484 of 3734 branches covered (93.3%)

7542 of 7864 relevant lines covered (95.91%)

4.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.77
/src/black/parsing.py
1
"""
2
Parse Python code and perform AST validation.
3
"""
4

5
import ast
5✔
6
import sys
5✔
7
import warnings
5✔
8
from collections.abc import Collection, Iterator
5✔
9

10
from black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature
5✔
11
from black.nodes import syms
5✔
12
from blib2to3 import pygram
5✔
13
from blib2to3.pgen2 import driver
5✔
14
from blib2to3.pgen2.grammar import Grammar
5✔
15
from blib2to3.pgen2.parse import ParseError
5✔
16
from blib2to3.pgen2.tokenize import TokenError
5✔
17
from blib2to3.pytree import Leaf, Node
5✔
18

19

20
class InvalidInput(ValueError):
5✔
21
    """Raised when input source code fails all parse attempts."""
22

23

24
def get_grammars(target_versions: set[TargetVersion]) -> list[Grammar]:
5✔
25
    if not target_versions:
5✔
26
        # No target_version specified, so try all grammars.
27
        return [
5✔
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 = []
5✔
37
    # If we have to parse both, try to parse async as a keyword first
38
    if not supports_feature(
5✔
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)
5✔
43
    if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
5✔
44
        # Python 3.0-3.6
45
        grammars.append(pygram.python_grammar)
5✔
46
    if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions):
5✔
47
        # Python 3.10+
48
        grammars.append(pygram.python_grammar_soft_keywords)
5✔
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
5✔
53

54

55
def lib2to3_parse(
5✔
56
    src_txt: str, target_versions: Collection[TargetVersion] = ()
57
) -> Node:
58
    """Given a string with source, return the lib2to3 Node."""
59
    if not src_txt.endswith("\n"):
5✔
60
        src_txt += "\n"
5✔
61

62
    grammars = get_grammars(set(target_versions))
5✔
63
    if target_versions:
5✔
64
        max_tv = max(target_versions, key=lambda tv: tv.value)
5✔
65
        tv_str = f" for target version {max_tv.pretty()}"
5✔
66
    else:
67
        tv_str = ""
5✔
68

69
    errors = {}
5✔
70
    for grammar in grammars:
5✔
71
        drv = driver.Driver(grammar)
5✔
72
        try:
5✔
73
            result = drv.parse_string(src_txt, False)
5✔
74
            break
5✔
75

76
        except ParseError as pe:
5✔
77
            lineno, column = pe.context[1]
5✔
78
            lines = src_txt.splitlines()
5✔
79
            try:
5✔
80
                faulty_line = lines[lineno - 1]
5✔
81
            except IndexError:
×
82
                faulty_line = "<line number missing in source>"
×
83
            errors[grammar.version] = InvalidInput(
5✔
84
                f"Cannot parse{tv_str}: {lineno}:{column}: {faulty_line}"
85
            )
86

87
        except TokenError as te:
5✔
88
            # In edge cases these are raised; and typically don't have a "faulty_line".
89
            lineno, column = te.args[1]
5✔
90
            errors[grammar.version] = InvalidInput(
5✔
91
                f"Cannot parse{tv_str}: {lineno}:{column}: {te.args[0]}"
92
            )
93

94
    else:
95
        # Choose the latest version when raising the actual parsing error.
96
        assert len(errors) >= 1
5✔
97
        exc = errors[max(errors)]
5✔
98
        raise exc from None
5✔
99

100
    if isinstance(result, Leaf):
5✔
101
        result = Node(syms.file_input, [result])
5✔
102
    return result
5✔
103

104

105
def matches_grammar(src_txt: str, grammar: Grammar) -> bool:
5✔
106
    drv = driver.Driver(grammar)
×
107
    try:
×
108
        drv.parse_string(src_txt, False)
×
109
    except (ParseError, TokenError, IndentationError):
×
110
        return False
×
111
    else:
112
        return True
×
113

114

115
def lib2to3_unparse(node: Node) -> str:
5✔
116
    """Given a lib2to3 node, return its string representation."""
117
    code = str(node)
×
118
    return code
×
119

120

121
class ASTSafetyError(Exception):
5✔
122
    """Raised when Black's generated code is not equivalent to the old AST."""
123

124

125
def _parse_single_version(
5✔
126
    src: str, version: tuple[int, int], *, type_comments: bool
127
) -> ast.AST:
128
    filename = "<unknown>"
5✔
129
    with warnings.catch_warnings():
5✔
130
        warnings.simplefilter("ignore", SyntaxWarning)
5✔
131
        warnings.simplefilter("ignore", DeprecationWarning)
5✔
132
        return ast.parse(
5✔
133
            src, filename, feature_version=version, type_comments=type_comments
134
        )
135

136

137
def parse_ast(src: str) -> ast.AST:
5✔
138
    # TODO: support Python 4+ ;)
139
    versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]
5✔
140

141
    first_error = ""
5✔
142
    for version in sorted(versions, reverse=True):
5✔
143
        try:
5✔
144
            return _parse_single_version(src, version, type_comments=True)
5✔
145
        except SyntaxError as e:
5✔
146
            if not first_error:
5✔
147
                first_error = str(e)
5✔
148

149
    # Try to parse without type comments
150
    for version in sorted(versions, reverse=True):
5✔
151
        try:
5✔
152
            return _parse_single_version(src, version, type_comments=False)
5✔
153
        except SyntaxError:
5✔
154
            pass
5✔
155

156
    raise SyntaxError(first_error)
5✔
157

158

159
def _normalize(lineend: str, value: str) -> str:
5✔
160
    # To normalize, we strip any leading and trailing space from
161
    # each line...
162
    stripped: list[str] = [i.strip() for i in value.splitlines()]
5✔
163
    normalized = lineend.join(stripped)
5✔
164
    # ...and remove any blank lines at the beginning and end of
165
    # the whole string
166
    return normalized.strip()
5✔
167

168

169
def stringify_ast(node: ast.AST) -> Iterator[str]:
5✔
170
    """Simple visitor generating strings to compare ASTs by content."""
171
    return _stringify_ast(node, [])
5✔
172

173

174
def _stringify_ast_with_new_parent(
5✔
175
    node: ast.AST, parent_stack: list[ast.AST], new_parent: ast.AST
176
) -> Iterator[str]:
177
    parent_stack.append(new_parent)
5✔
178
    yield from _stringify_ast(node, parent_stack)
5✔
179
    parent_stack.pop()
5✔
180

181

182
def _stringify_ast(node: ast.AST, parent_stack: list[ast.AST]) -> Iterator[str]:
5✔
183
    if (
5✔
184
        isinstance(node, ast.Constant)
185
        and isinstance(node.value, str)
186
        and node.kind == "u"
187
    ):
188
        # It's a quirk of history that we strip the u prefix over here. We used to
189
        # rewrite the AST nodes for Python version compatibility and we never copied
190
        # over the kind
191
        node.kind = None
5✔
192

193
    yield f"{'    ' * len(parent_stack)}{node.__class__.__name__}("
5✔
194

195
    for field in sorted(node._fields):  # noqa: F402
5✔
196
        # TypeIgnore has only one field 'lineno' which breaks this comparison
197
        if isinstance(node, ast.TypeIgnore):
5✔
198
            break
5✔
199

200
        try:
5✔
201
            value: object = getattr(node, field)
5✔
202
        except AttributeError:
×
203
            continue
×
204

205
        yield f"{'    ' * (len(parent_stack) + 1)}{field}="
5✔
206

207
        if isinstance(value, list):
5✔
208
            for item in value:
5✔
209
                # Ignore nested tuples within del statements, because we may insert
210
                # parentheses and they change the AST.
211
                if (
5✔
212
                    field == "targets"
213
                    and isinstance(node, ast.Delete)
214
                    and isinstance(item, ast.Tuple)
215
                ):
216
                    for elt in _unwrap_tuples(item):
5✔
217
                        yield from _stringify_ast_with_new_parent(
5✔
218
                            elt, parent_stack, node
219
                        )
220

221
                elif isinstance(item, ast.AST):
5✔
222
                    yield from _stringify_ast_with_new_parent(item, parent_stack, node)
5✔
223

224
        elif isinstance(value, ast.AST):
5✔
225
            yield from _stringify_ast_with_new_parent(value, parent_stack, node)
5✔
226

227
        else:
228
            normalized: object
229
            if (
5✔
230
                isinstance(node, ast.Constant)
231
                and field == "value"
232
                and isinstance(value, str)
233
                and len(parent_stack) >= 2
234
                # Any standalone string, ideally this would
235
                # exactly match black.nodes.is_docstring
236
                and isinstance(parent_stack[-1], ast.Expr)
237
            ):
238
                # Constant strings may be indented across newlines, if they are
239
                # docstrings; fold spaces after newlines when comparing. Similarly,
240
                # trailing and leading space may be removed.
241
                normalized = _normalize("\n", value)
5✔
242
            elif field == "type_comment" and isinstance(value, str):
5✔
243
                # Trailing whitespace in type comments is removed.
244
                normalized = value.rstrip()
5✔
245
            else:
246
                normalized = value
5✔
247
            yield (
5✔
248
                f"{'    ' * (len(parent_stack) + 1)}{normalized!r},  #"
249
                f" {value.__class__.__name__}"
250
            )
251

252
    yield f"{'    ' * len(parent_stack)})  # /{node.__class__.__name__}"
5✔
253

254

255
def _unwrap_tuples(node: ast.Tuple) -> Iterator[ast.AST]:
5✔
256
    for elt in node.elts:
5✔
257
        if isinstance(elt, ast.Tuple):
5✔
258
            yield from _unwrap_tuples(elt)
5✔
259
        else:
260
            yield elt
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc