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

psf / black / 6218957245

18 Sep 2023 06:24AM UTC coverage: 96.528%. Remained the same
6218957245

Pull #3890

github

web-flow
Bump docker/setup-qemu-action from 2 to 3

Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3890: Bump docker/setup-qemu-action from 2 to 3

6616 of 6854 relevant lines covered (96.53%)

3.86 hits per line

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

94.78
/src/black/parsing.py
1
"""
2
Parse Python code and perform AST validation.
3
"""
4
import ast
4✔
5
import sys
4✔
6
from typing import Final, Iterable, Iterator, List, Set, Tuple
4✔
7

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

17
PY2_HINT: Final = "Python 2 support was removed in version 22.0."
4✔
18

19

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

23

24
def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]:
4✔
25
    if not target_versions:
4✔
26
        # No target_version specified, so try all grammars.
27
        return [
4✔
28
            # Python 3.7-3.9
29
            pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords,
30
            # Python 3.0-3.6
31
            pygram.python_grammar_no_print_statement_no_exec_statement,
32
            # Python 3.10+
33
            pygram.python_grammar_soft_keywords,
34
        ]
35

36
    grammars = []
4✔
37
    # If we have to parse both, try to parse async as a keyword first
38
    if not supports_feature(
4✔
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(
4✔
43
            pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords
44
        )
45
    if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
4✔
46
        # Python 3.0-3.6
47
        grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement)
4✔
48
    if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions):
4✔
49
        # Python 3.10+
50
        grammars.append(pygram.python_grammar_soft_keywords)
4✔
51

52
    # At least one of the above branches must have been taken, because every Python
53
    # version has exactly one of the two 'ASYNC_*' flags
54
    return grammars
4✔
55

56

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

62
    grammars = get_grammars(set(target_versions))
4✔
63
    errors = {}
4✔
64
    for grammar in grammars:
4✔
65
        drv = driver.Driver(grammar)
4✔
66
        try:
4✔
67
            result = drv.parse_string(src_txt, True)
4✔
68
            break
4✔
69

70
        except ParseError as pe:
4✔
71
            lineno, column = pe.context[1]
4✔
72
            lines = src_txt.splitlines()
4✔
73
            try:
4✔
74
                faulty_line = lines[lineno - 1]
4✔
75
            except IndexError:
×
76
                faulty_line = "<line number missing in source>"
×
77
            errors[grammar.version] = InvalidInput(
4✔
78
                f"Cannot parse: {lineno}:{column}: {faulty_line}"
79
            )
80

81
        except TokenError as te:
4✔
82
            # In edge cases these are raised; and typically don't have a "faulty_line".
83
            lineno, column = te.args[1]
4✔
84
            errors[grammar.version] = InvalidInput(
4✔
85
                f"Cannot parse: {lineno}:{column}: {te.args[0]}"
86
            )
87

88
    else:
89
        # Choose the latest version when raising the actual parsing error.
90
        assert len(errors) >= 1
4✔
91
        exc = errors[max(errors)]
4✔
92

93
        if matches_grammar(src_txt, pygram.python_grammar) or matches_grammar(
4✔
94
            src_txt, pygram.python_grammar_no_print_statement
95
        ):
96
            original_msg = exc.args[0]
4✔
97
            msg = f"{original_msg}\n{PY2_HINT}"
4✔
98
            raise InvalidInput(msg) from None
4✔
99

100
        raise exc from None
4✔
101

102
    if isinstance(result, Leaf):
4✔
103
        result = Node(syms.file_input, [result])
4✔
104
    return result
4✔
105

106

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

116

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

122

123
def parse_single_version(
4✔
124
    src: str, version: Tuple[int, int], *, type_comments: bool
125
) -> ast.AST:
126
    filename = "<unknown>"
4✔
127
    return ast.parse(
4✔
128
        src, filename, feature_version=version, type_comments=type_comments
129
    )
130

131

132
def parse_ast(src: str) -> ast.AST:
4✔
133
    # TODO: support Python 4+ ;)
134
    versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]
4✔
135

136
    first_error = ""
4✔
137
    for version in sorted(versions, reverse=True):
4✔
138
        try:
4✔
139
            return parse_single_version(src, version, type_comments=True)
4✔
140
        except SyntaxError as e:
4✔
141
            if not first_error:
4✔
142
                first_error = str(e)
4✔
143

144
    # Try to parse without type comments
145
    for version in sorted(versions, reverse=True):
4✔
146
        try:
4✔
147
            return parse_single_version(src, version, type_comments=False)
4✔
148
        except SyntaxError:
4✔
149
            pass
4✔
150

151
    raise SyntaxError(first_error)
4✔
152

153

154
def _normalize(lineend: str, value: str) -> str:
4✔
155
    # To normalize, we strip any leading and trailing space from
156
    # each line...
157
    stripped: List[str] = [i.strip() for i in value.splitlines()]
4✔
158
    normalized = lineend.join(stripped)
4✔
159
    # ...and remove any blank lines at the beginning and end of
160
    # the whole string
161
    return normalized.strip()
4✔
162

163

164
def stringify_ast(node: ast.AST, depth: int = 0) -> Iterator[str]:
4✔
165
    """Simple visitor generating strings to compare ASTs by content."""
166

167
    if (
4✔
168
        isinstance(node, ast.Constant)
169
        and isinstance(node.value, str)
170
        and node.kind == "u"
171
    ):
172
        # It's a quirk of history that we strip the u prefix over here. We used to
173
        # rewrite the AST nodes for Python version compatibility and we never copied
174
        # over the kind
175
        node.kind = None
4✔
176

177
    yield f"{'  ' * depth}{node.__class__.__name__}("
4✔
178

179
    for field in sorted(node._fields):  # noqa: F402
4✔
180
        # TypeIgnore has only one field 'lineno' which breaks this comparison
181
        if isinstance(node, ast.TypeIgnore):
4✔
182
            break
4✔
183

184
        try:
4✔
185
            value: object = getattr(node, field)
4✔
186
        except AttributeError:
×
187
            continue
×
188

189
        yield f"{'  ' * (depth+1)}{field}="
4✔
190

191
        if isinstance(value, list):
4✔
192
            for item in value:
4✔
193
                # Ignore nested tuples within del statements, because we may insert
194
                # parentheses and they change the AST.
195
                if (
4✔
196
                    field == "targets"
197
                    and isinstance(node, ast.Delete)
198
                    and isinstance(item, ast.Tuple)
199
                ):
200
                    for elt in item.elts:
4✔
201
                        yield from stringify_ast(elt, depth + 2)
4✔
202

203
                elif isinstance(item, ast.AST):
4✔
204
                    yield from stringify_ast(item, depth + 2)
4✔
205

206
        elif isinstance(value, ast.AST):
4✔
207
            yield from stringify_ast(value, depth + 2)
4✔
208

209
        else:
210
            normalized: object
211
            if (
4✔
212
                isinstance(node, ast.Constant)
213
                and field == "value"
214
                and isinstance(value, str)
215
            ):
216
                # Constant strings may be indented across newlines, if they are
217
                # docstrings; fold spaces after newlines when comparing. Similarly,
218
                # trailing and leading space may be removed.
219
                normalized = _normalize("\n", value)
4✔
220
            elif field == "type_comment" and isinstance(value, str):
4✔
221
                # Trailing whitespace in type comments is removed.
222
                normalized = value.rstrip()
4✔
223
            else:
224
                normalized = value
4✔
225
            yield f"{'  ' * (depth+2)}{normalized!r},  # {value.__class__.__name__}"
4✔
226

227
    yield f"{'  ' * depth})  # /{node.__class__.__name__}"
4✔
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