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

zopefoundation / RestrictedPython / 26870188116

03 Jun 2026 07:27AM UTC coverage: 98.978% (+0.006%) from 98.972%
26870188116

Pull #317

github

web-flow
Merge b1d1303c6 into a2891c0d1
Pull Request #317: Type Annotations for RestrictedPython

207 of 209 branches covered (99.04%)

153 of 154 new or added lines in 7 files covered. (99.35%)

3 existing lines in 2 files now uncovered.

2518 of 2544 relevant lines covered (98.98%)

0.99 hits per line

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

99.56
/src/RestrictedPython/transformer.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE
11
#
12
##############################################################################
13
"""
14
transformer module:
15

16
uses Python standard library ast module and its containing classes to transform
17
the parsed python code to create a modified AST for a byte code generation.
18
"""
19

20

21
import ast
1✔
22
import contextlib
1✔
23
import textwrap
1✔
24
import typing
1✔
25

26

27
# For AugAssign the operator must be converted to a string.
28
IOPERATOR_TO_STR = {
1✔
29
    ast.Add: '+=',
30
    ast.Sub: '-=',
31
    ast.Mult: '*=',
32
    ast.Div: '/=',
33
    ast.Mod: '%=',
34
    ast.Pow: '**=',
35
    ast.LShift: '<<=',
36
    ast.RShift: '>>=',
37
    ast.BitOr: '|=',
38
    ast.BitXor: '^=',
39
    ast.BitAnd: '&=',
40
    ast.FloorDiv: '//=',
41
    ast.MatMult: '@=',
42
}
43

44
# For creation allowed magic method names. See also
45
# https://docs.python.org/3/reference/datamodel.html#special-method-names
46
ALLOWED_FUNC_NAMES = frozenset([
1✔
47
    '__init__',
48
    '__contains__',
49
    '__lt__',
50
    '__le__',
51
    '__eq__',
52
    '__ne__',
53
    '__gt__',
54
    '__ge__',
55
])
56

57

58
FORBIDDEN_FUNC_NAMES = frozenset([
1✔
59
    'print',
60
    'printed',
61
    'builtins',
62
    'breakpoint',
63
])
64

65
# Attributes documented in the `inspect` module, but defined on the listed
66
# objects. See also https://docs.python.org/3/library/inspect.html
67
INSPECT_ATTRIBUTES = frozenset([
1✔
68
    # on traceback objects:
69
    "tb_frame",
70
    # "tb_lasti",  # int
71
    # "tb_lineno",  # int
72
    "tb_next",
73
    # on frame objects:
74
    "f_back",
75
    "f_builtins",
76
    "f_code",
77
    "f_generator",
78
    "f_globals",
79
    # "f_lasti",  # int
80
    # "f_lineno",  # int
81
    "f_locals",
82
    "f_trace",
83
    # on code objects:
84
    # "co_argcount",  # int
85
    "co_code",
86
    # "co_cellvars",  # tuple of str
87
    # "co_consts",   # tuple of str
88
    # "co_filename",  # str
89
    # "co_firstlineno",  # int
90
    # "co_flags",  # int
91
    # "co_lnotab",  # mapping between ints and indices
92
    # "co_freevars",  # tuple of strings
93
    # "co_posonlyargcount",  # int
94
    # "co_kwonlyargcount",  # int
95
    # "co_name",  # str
96
    # "co_qualname",  # str
97
    # "co_names",  # str
98
    # "co_nlocals",  # int
99
    # "co_stacksize",  # int
100
    # "co_varnames",  # tuple of str
101
    # on generator objects:
102
    "gi_frame",
103
    # "gi_running",  # bool
104
    # "gi_suspended",  # bool
105
    "gi_code",
106
    "gi_yieldfrom",
107
    # on coroutine objects:
108
    "cr_await",
109
    "cr_frame",
110
    # "cr_running",  # bool
111
    "cr_code",
112
    "cr_origin",
113
])
114

115
_T_visit_return: typing.TypeAlias = ast.AST | list[ast.AST] | None
1✔
116

117
# When new ast nodes are generated they have no 'lineno', 'end_lineno',
118
# 'col_offset' and 'end_col_offset'. This function copies these fields from the
119
# incoming node:
120

121

122
def copy_locations(new_node: ast.AST, old_node: ast.AST) -> None:
1✔
123
    assert 'lineno' in new_node._attributes
1✔
124
    new_node.lineno = old_node.lineno
1✔
125

126
    assert 'end_lineno' in new_node._attributes
1✔
127
    new_node.end_lineno = old_node.end_lineno
1✔
128

129
    assert 'col_offset' in new_node._attributes
1✔
130
    new_node.col_offset = old_node.col_offset
1✔
131

132
    assert 'end_col_offset' in new_node._attributes
1✔
133
    new_node.end_col_offset = old_node.end_col_offset
1✔
134

135
    ast.fix_missing_locations(new_node)
1✔
136

137

138
class PrintInfo:
1✔
139
    def __init__(self):
1✔
140
        self.print_used = False
1✔
141
        self.printed_used = False
1✔
142

143
    @contextlib.contextmanager
1✔
144
    def new_print_scope(self):
1✔
145
        old_print_used = self.print_used
1✔
146
        old_printed_used = self.printed_used
1✔
147

148
        self.print_used = False
1✔
149
        self.printed_used = False
1✔
150

151
        try:
1✔
152
            yield
1✔
153
        finally:
154
            self.print_used = old_print_used
1✔
155
            self.printed_used = old_printed_used
1✔
156

157

158
class RestrictingNodeTransformer(ast.NodeTransformer):
1✔
159
    errors: list[str]
1✔
160
    warnings: list[str]
1✔
161
    used_names: dict[str, bool]
1✔
162

163
    def __init__(self,
1✔
164
                 errors: list[str] | None = None,
165
                 warnings: list[str] | None = None,
166
                 used_names: dict[str, bool] | None = None):
167
        super().__init__()
1✔
168
        self.errors = [] if errors is None else errors
1✔
169
        self.warnings = [] if warnings is None else warnings
1✔
170

171
        # All the variables used by the incoming source.
172
        # Internal names/variables, like the ones from 'gen_tmp_name', don't
173
        # have to be added.
174
        # 'used_names' is for example needed by 'RestrictionCapableEval' to
175
        # know wich names it has to supply when calling the final code.
176
        self.used_names = {} if used_names is None else used_names
1✔
177

178
        # Global counter to construct temporary variable names.
179
        self._tmp_idx = 0
1✔
180

181
        self.print_info = PrintInfo()
1✔
182

183
    def gen_tmp_name(self) -> str:
1✔
184
        # 'check_name' ensures that no variable is prefixed with '_'.
185
        # => Its safe to use '_tmp..' as a temporary variable.
186
        name = '_tmp%i' % self._tmp_idx
1✔
187
        self._tmp_idx += 1
1✔
188
        return name
1✔
189

190
    def error(self, node: ast.AST, info: str) -> None:
1✔
191
        """Record a security error discovered during transformation."""
192
        lineno = getattr(node, 'lineno', None)
1✔
193
        self.errors.append(
1✔
194
            f'Line {lineno}: {info}')
195

196
    def warn(self, node: ast.AST, info: str) -> None:
1✔
197
        """Record a security warning discovered during transformation."""
198
        lineno = getattr(node, 'lineno', None)
1✔
199
        self.warnings.append(
1✔
200
            f'Line {lineno}: {info}')
201

202
    def guard_iter(self, node: ast.AST) -> ast.AST:
1✔
203
        """
204
        Converts:
205
            for x in expr
206
        to
207
            for x in _getiter_(expr)
208

209
        Also used for
210
        * list comprehensions
211
        * dict comprehensions
212
        * set comprehensions
213
        * generator expresions
214
        """
215
        node = self.node_contents_visit(node)
1✔
216

217
        if isinstance(node.target, ast.Tuple):
1✔
218
            spec = self.gen_unpack_spec(node.target)
1✔
219
            new_iter = ast.Call(
1✔
220
                func=ast.Name('_iter_unpack_sequence_', ast.Load()),
221
                args=[node.iter, spec, ast.Name('_getiter_', ast.Load())],
222
                keywords=[])
223
        else:
224
            new_iter = ast.Call(
1✔
225
                func=ast.Name("_getiter_", ast.Load()),
226
                args=[node.iter],
227
                keywords=[])
228

229
        copy_locations(new_iter, node.iter)
1✔
230
        node.iter = new_iter
1✔
231
        return node
1✔
232

233
    def is_starred(self, ob: ast.AST) -> bool:
1✔
234
        return isinstance(ob, ast.Starred)
1✔
235

236
    def gen_unpack_spec(self, tpl: ast.Tuple) -> ast.Dict:
1✔
237
        """Generate a specification for 'guarded_unpack_sequence'.
238

239
        This spec is used to protect sequence unpacking.
240
        The primary goal of this spec is to tell which elements in a sequence
241
        are sequences again. These 'child' sequences have to be protected
242
        again.
243

244
        For example there is a sequence like this:
245
            (a, (b, c), (d, (e, f))) = g
246

247
        On a higher level the spec says:
248
            - There is a sequence of len 3
249
            - The element at index 1 is a sequence again with len 2
250
            - The element at index 2 is a sequence again with len 2
251
              - The element at index 1 in this subsequence is a sequence again
252
                with len 2
253

254
        With this spec 'guarded_unpack_sequence' does something like this for
255
        protection (len checks are omitted):
256

257
            t = list(_getiter_(g))
258
            t[1] = list(_getiter_(t[1]))
259
            t[2] = list(_getiter_(t[2]))
260
            t[2][1] = list(_getiter_(t[2][1]))
261
            return t
262

263
        The 'real' spec for the case above is then:
264
            spec = {
265
                'min_len': 3,
266
                'childs': (
267
                    (1, {'min_len': 2, 'childs': ()}),
268
                    (2, {
269
                            'min_len': 2,
270
                            'childs': (
271
                                (1, {'min_len': 2, 'childs': ()})
272
                            )
273
                        }
274
                    )
275
                )
276
            }
277

278
        So finally the assignment above is converted into:
279
            (a, (b, c), (d, (e, f))) = guarded_unpack_sequence(g, spec)
280
        """
281
        spec = ast.Dict(keys=[], values=[])
1✔
282

283
        spec.keys.append(ast.Constant('childs'))
1✔
284
        spec.values.append(ast.Tuple([], ast.Load()))
1✔
285

286
        # starred elements in a sequence do not contribute into the min_len.
287
        # For example a, b, *c = g
288
        # g must have at least 2 elements, not 3. 'c' is empyt if g has only 2.
289
        min_len = len([ob for ob in tpl.elts if not self.is_starred(ob)])
1✔
290
        offset = 0
1✔
291

292
        for idx, val in enumerate(tpl.elts):
1✔
293
            # After a starred element specify the child index from the back.
294
            # Since it is unknown how many elements from the sequence are
295
            # consumed by the starred element.
296
            # For example a, *b, (c, d) = g
297
            # Then (c, d) has the index '-1'
298
            if self.is_starred(val):
1✔
299
                offset = min_len + 1
1✔
300

301
            elif isinstance(val, ast.Tuple):
1✔
302
                el = ast.Tuple([], ast.Load())
1✔
303
                el.elts.append(ast.Constant(idx - offset))
1✔
304
                el.elts.append(self.gen_unpack_spec(val))
1✔
305
                spec.values[0].elts.append(el)
1✔
306

307
        spec.keys.append(ast.Constant('min_len'))
1✔
308
        spec.values.append(ast.Constant(min_len))
1✔
309

310
        return spec
1✔
311

312
    def protect_unpack_sequence(
1✔
313
            self,
314
            target: ast.Tuple,
315
            value: ast.AST) -> ast.Call:
316
        spec = self.gen_unpack_spec(target)
1✔
317
        return ast.Call(
1✔
318
            func=ast.Name('_unpack_sequence_', ast.Load()),
319
            args=[value, spec, ast.Name('_getiter_', ast.Load())],
320
            keywords=[])
321

322
    def gen_unpack_wrapper(self, node: ast.AST,
1✔
323
                           target: ast.Tuple) -> tuple[ast.Name, ast.Try]:
324
        """Helper function to protect tuple unpacks.
325

326
        node: used to copy the locations for the new nodes.
327
        target: is the tuple which must be protected.
328

329
        It returns a tuple with two element.
330

331
        Element 1: Is a temporary name node which must be used to
332
                   replace the target.
333
                   The context (store, param) is defined
334
                   by the 'ctx' parameter..
335

336
        Element 2: Is a try .. finally where the body performs the
337
                   protected tuple unpack of the temporary variable
338
                   into the original target.
339
        """
340

341
        # Generate a tmp name to replace the tuple with.
342
        tmp_name = self.gen_tmp_name()
1✔
343

344
        # Generates an expressions which protects the unpack.
345
        # converter looks like 'wrapper(tmp_name)'.
346
        # 'wrapper' takes care to protect sequence unpacking with _getiter_.
347
        converter = self.protect_unpack_sequence(
1✔
348
            target,
349
            ast.Name(tmp_name, ast.Load()))
350

351
        # Assign the expression to the original names.
352
        # Cleanup the temporary variable.
353
        # Generates:
354
        # try:
355
        #     # converter is 'wrapper(tmp_name)'
356
        #     arg = converter
357
        # finally:
358
        #     del tmp_arg
359
        try_body = [ast.Assign(targets=[target], value=converter)]
1✔
360
        finalbody = [self.gen_del_stmt(tmp_name)]
1✔
361
        cleanup = ast.Try(
1✔
362
            body=try_body, finalbody=finalbody, handlers=[], orelse=[])
363

364
        # This node is used to catch the tuple in a tmp variable.
365
        tmp_target = ast.Name(tmp_name, ast.Store())
1✔
366

367
        copy_locations(tmp_target, node)
1✔
368
        copy_locations(cleanup, node)
1✔
369

370
        return (tmp_target, cleanup)
1✔
371

372
    def gen_none_node(self) -> ast.Constant:
1✔
373
        return ast.Constant(None)
1✔
374

375
    def gen_del_stmt(self, name_to_del: str) -> ast.Delete:
1✔
376
        return ast.Delete(targets=[ast.Name(name_to_del, ast.Del())])
1✔
377

378
    def check_name(
1✔
379
            self,
380
            node: ast.AST,
381
            name: str,
382
            allow_magic_methods: bool = False) -> None:
383
        """Check names if they are allowed.
384

385
        If ``allow_magic_methods is True`` names in `ALLOWED_FUNC_NAMES`
386
        are additionally allowed although their names start with `_`.
387

388
        """
389
        if name is None:
1✔
390
            return
1✔
391

392
        if (name.startswith('_')
1✔
393
                and name != '_'
394
                and not (allow_magic_methods
395
                         and name in ALLOWED_FUNC_NAMES
396
                         and node.col_offset != 0)):
397
            self.error(
1✔
398
                node,
399
                '"{name}" is an invalid variable name because it '
400
                'starts with "_"'.format(name=name))
401
        elif name.endswith('__roles__'):
1✔
402
            self.error(node, '"%s" is an invalid variable name because '
1✔
403
                       'it ends with "__roles__".' % name)
404
        elif name in FORBIDDEN_FUNC_NAMES:
1✔
405
            self.error(node, f'"{name}" is a reserved name.')
1✔
406

407
    def check_function_argument_names(self, node: ast.FunctionDef) -> None:
1✔
408
        for arg in node.args.args:
1✔
409
            self.check_name(node, arg.arg)
1✔
410

411
        if node.args.vararg:
1✔
412
            self.check_name(node, node.args.vararg.arg)
1✔
413

414
        if node.args.kwarg:
1✔
415
            self.check_name(node, node.args.kwarg.arg)
1✔
416

417
        for arg in node.args.kwonlyargs:
1✔
418
            self.check_name(node, arg.arg)
1✔
419

420
    def check_import_names(self, node: ast.ImportFrom | ast.Import) -> ast.AST:
1✔
421
        """Check the names being imported.
422

423
        This is a protection against rebinding dunder names like
424
        _getitem_, _write_ via imports.
425

426
        => 'from _a import x' is ok, because '_a' is not added to the scope.
427
        """
428
        for name in node.names:
1✔
429
            if '*' in name.name:
1✔
430
                self.error(node, '"*" imports are not allowed.')
1✔
431
            self.check_name(node, name.name)
1✔
432
            if name.asname:
1✔
433
                self.check_name(node, name.asname)
1✔
434

435
        return self.node_contents_visit(node)
1✔
436

437
    def inject_print_collector(self, node: ast.AST, position: int = 0) -> None:
1✔
438
        print_used = self.print_info.print_used
1✔
439
        printed_used = self.print_info.printed_used
1✔
440

441
        if print_used or printed_used:
1✔
442
            # Add '_print = _print_(_getattr_)' add the top of a
443
            # function/module.
444
            _print = ast.Assign(
1✔
445
                targets=[ast.Name('_print', ast.Store())],
446
                value=ast.Call(
447
                    func=ast.Name("_print_", ast.Load()),
448
                    args=[ast.Name("_getattr_", ast.Load())],
449
                    keywords=[]))
450

451
            if isinstance(node, ast.Module):
1✔
452
                _print.lineno = position
1✔
453
                _print.col_offset = position
1✔
454
                _print.end_lineno = position
1✔
455
                _print.end_col_offset = position
1✔
456
                ast.fix_missing_locations(_print)
1✔
457
            else:
458
                copy_locations(_print, node)
1✔
459

460
            node.body.insert(position, _print)
1✔
461

462
            if not printed_used:
1✔
463
                self.warn(node, "Prints, but never reads 'printed' variable.")
1✔
464

465
            elif not print_used:
1✔
466
                self.warn(node, "Doesn't print, but reads 'printed' variable.")
1✔
467

468
    # Special Functions for an ast.NodeTransformer
469

470
    def generic_visit(self, node: ast.AST) -> ast.AST | None:
1✔
471
        """Reject ast nodes which do not have a corresponding `visit_` method.
472

473
        This is needed to prevent new ast nodes from new Python versions to be
474
        trusted before any security review.
475

476
        To access `generic_visit` on the super class use `node_contents_visit`.
477
        """
478
        self.warn(
1✔
479
            node,
480
            '{0.__class__.__name__}'
481
            ' statement is not known to RestrictedPython'.format(node)
482
        )
483
        self.not_allowed(node)
1✔
484

485
    def not_allowed(self, node: ast.AST) -> None:
1✔
486
        self.error(
1✔
487
            node,
488
            f'{node.__class__.__name__} statements are not allowed.')
489

490
    def node_contents_visit(self, node: ast.AST) -> ast.AST:
1✔
491
        """Visit the contents of a node."""
492
        return super().generic_visit(node)
1✔
493

494
    # ast for Literals
495

496
    def visit_Constant(self, node: ast.Constant) -> _T_visit_return:
1✔
497
        """Allow constant literals.
498

499
        Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in
500
        Python 3.8+.
501
        :see: https://docs.python.org/dev/whatsnew/3.8.html#deprecated
502
        """
503
        return self.node_contents_visit(node)
1✔
504

505
    def visit_Interactive(self, node: ast.Interactive) -> _T_visit_return:
1✔
506
        """Allow single mode without restrictions."""
507
        return self.node_contents_visit(node)
1✔
508

509
    def visit_List(self, node: ast.List) -> _T_visit_return:
1✔
510
        """Allow list literals without restrictions."""
511
        return self.node_contents_visit(node)
1✔
512

513
    def visit_Tuple(self, node: ast.Tuple) -> _T_visit_return:
1✔
514
        """Allow tuple literals without restrictions."""
515
        return self.node_contents_visit(node)
1✔
516

517
    def visit_Set(self, node: ast.Set) -> _T_visit_return:
1✔
518
        """Allow set literals without restrictions."""
519
        return self.node_contents_visit(node)
1✔
520

521
    def visit_Dict(self, node: ast.Dict) -> _T_visit_return:
1✔
522
        """Allow dict literals without restrictions."""
523
        return self.node_contents_visit(node)
1✔
524

525
    def visit_FormattedValue(
1✔
526
            self,
527
            node: ast.FormattedValue) -> _T_visit_return:
528
        """Allow f-strings without restrictions."""
529
        return self.node_contents_visit(node)
1✔
530

531
    def visit_TemplateStr(self, node: ast.AST) -> _T_visit_return:
1✔
532
        """Template strings are allowed by default.
533

534
        As Template strings are a very basic template mechanism, that needs
535
        additional rendering logic to be useful, they are not blocked by
536
        default.
537
        Those rendering logic would be affected by RestrictedPython as well.
538

539
        TODO: Change Type Annotation to ast.TemplateStr when
540
              Support for Python 3.13 is dropped.
541
        """
UNCOV
542
        return self.node_contents_visit(node)
×
543

544
    def visit_Interpolation(self, node: ast.AST) -> _T_visit_return:
1✔
545
        """Interpolations are allowed by default.
546

547
        As Interpolations are part of Template Strings, they are needed
548
        to be reached in the context of RestrictedPython as Template Strings
549
        are allowed. As a user has to provide additional rendering logic
550
        to make use of Template Strings, the security implications of
551
        Interpolations are limited in the context of RestrictedPython.
552

553
        TODO: Change Type Annotation to ast.Interpolation when
554
              Support for Python 3.13 is dropped.
555
        """
UNCOV
556
        return self.node_contents_visit(node)
×
557

558
    def visit_JoinedStr(self, node: ast.JoinedStr) -> _T_visit_return:
1✔
559
        """Allow joined string without restrictions."""
560
        return self.node_contents_visit(node)
1✔
561

562
    # ast for Variables
563

564
    def visit_Name(self, node: ast.Name) -> _T_visit_return:
1✔
565
        """Prevents access to protected names.
566

567
        Converts use of the name 'printed' to this expression: '_print()'
568
        """
569

570
        node = self.node_contents_visit(node)
1✔
571

572
        if isinstance(node.ctx, ast.Load):
1✔
573
            if node.id == 'printed':
1✔
574
                self.print_info.printed_used = True
1✔
575
                new_node = ast.Call(
1✔
576
                    func=ast.Name("_print", ast.Load()),
577
                    args=[],
578
                    keywords=[])
579

580
                copy_locations(new_node, node)
1✔
581
                return new_node
1✔
582

583
            elif node.id == 'print':
1✔
584
                self.print_info.print_used = True
1✔
585
                new_node = ast.Attribute(
1✔
586
                    value=ast.Name('_print', ast.Load()),
587
                    attr="_call_print",
588
                    ctx=ast.Load())
589

590
                copy_locations(new_node, node)
1✔
591
                return new_node
1✔
592

593
            self.used_names[node.id] = True
1✔
594

595
        self.check_name(node, node.id)
1✔
596
        return node
1✔
597

598
    def visit_Load(self, node: ast.Load) -> _T_visit_return:
1✔
599
        """
600

601
        """
602
        return self.node_contents_visit(node)
1✔
603

604
    def visit_Store(self, node: ast.Store) -> _T_visit_return:
1✔
605
        """
606

607
        """
608
        return self.node_contents_visit(node)
1✔
609

610
    def visit_Del(self, node: ast.Del) -> _T_visit_return:
1✔
611
        """
612

613
        """
614
        return self.node_contents_visit(node)
1✔
615

616
    def visit_Starred(self, node: ast.Starred) -> _T_visit_return:
1✔
617
        """
618

619
        """
620
        return self.node_contents_visit(node)
1✔
621

622
    # Expressions
623

624
    def visit_Expression(self, node: ast.Expression) -> _T_visit_return:
1✔
625
        """Allow Expression statements without restrictions.
626

627
        They are in the AST when using the `eval` compile mode.
628
        """
629
        return self.node_contents_visit(node)
1✔
630

631
    def visit_Expr(self, node: ast.Expr) -> _T_visit_return:
1✔
632
        """Allow Expr statements (any expression) without restrictions."""
633
        return self.node_contents_visit(node)
1✔
634

635
    def visit_UnaryOp(self, node: ast.UnaryOp) -> _T_visit_return:
1✔
636
        """
637
        UnaryOp (Unary Operations) is the overall element for:
638
        * Not --> which should be allowed
639
        * UAdd --> Positive notation of variables (e.g. +var)
640
        * USub --> Negative notation of variables (e.g. -var)
641
        """
642
        return self.node_contents_visit(node)
1✔
643

644
    def visit_UAdd(self, node: ast.UAdd) -> _T_visit_return:
1✔
645
        """Allow positive notation of variables. (e.g. +var)"""
646
        return self.node_contents_visit(node)
1✔
647

648
    def visit_USub(self, node: ast.USub) -> _T_visit_return:
1✔
649
        """Allow negative notation of variables. (e.g. -var)"""
650
        return self.node_contents_visit(node)
1✔
651

652
    def visit_Not(self, node: ast.Not) -> _T_visit_return:
1✔
653
        """Allow the `not` operator."""
654
        return self.node_contents_visit(node)
1✔
655

656
    def visit_Invert(self, node: ast.Invert) -> _T_visit_return:
1✔
657
        """Allow `~` expressions."""
658
        return self.node_contents_visit(node)
1✔
659

660
    def visit_BinOp(self, node: ast.BinOp) -> _T_visit_return:
1✔
661
        """Allow binary operations."""
662
        return self.node_contents_visit(node)
1✔
663

664
    def visit_Add(self, node: ast.Add) -> _T_visit_return:
1✔
665
        """Allow `+` expressions."""
666
        return self.node_contents_visit(node)
1✔
667

668
    def visit_Sub(self, node: ast.Sub) -> _T_visit_return:
1✔
669
        """Allow `-` expressions."""
670
        return self.node_contents_visit(node)
1✔
671

672
    def visit_Mult(self, node: ast.Mult) -> _T_visit_return:
1✔
673
        """Allow `*` expressions."""
674
        return self.node_contents_visit(node)
1✔
675

676
    def visit_Div(self, node: ast.Div) -> _T_visit_return:
1✔
677
        """Allow `/` expressions."""
678
        return self.node_contents_visit(node)
1✔
679

680
    def visit_FloorDiv(self, node: ast.FloorDiv) -> _T_visit_return:
1✔
681
        """Allow `//` expressions."""
682
        return self.node_contents_visit(node)
1✔
683

684
    def visit_Mod(self, node: ast.Mod) -> _T_visit_return:
1✔
685
        """Allow `%` expressions."""
686
        return self.node_contents_visit(node)
1✔
687

688
    def visit_Pow(self, node: ast.Pow) -> _T_visit_return:
1✔
689
        """Allow `**` expressions."""
690
        return self.node_contents_visit(node)
1✔
691

692
    def visit_LShift(self, node: ast.LShift) -> _T_visit_return:
1✔
693
        """Allow `<<` expressions."""
694
        return self.node_contents_visit(node)
1✔
695

696
    def visit_RShift(self, node: ast.RShift) -> _T_visit_return:
1✔
697
        """Allow `>>` expressions."""
698
        return self.node_contents_visit(node)
1✔
699

700
    def visit_BitOr(self, node: ast.BitOr) -> _T_visit_return:
1✔
701
        """Allow `|` expressions."""
702
        return self.node_contents_visit(node)
1✔
703

704
    def visit_BitXor(self, node: ast.BitXor) -> _T_visit_return:
1✔
705
        """Allow `^` expressions."""
706
        return self.node_contents_visit(node)
1✔
707

708
    def visit_BitAnd(self, node: ast.BitAnd) -> _T_visit_return:
1✔
709
        """Allow `&` expressions."""
710
        return self.node_contents_visit(node)
1✔
711

712
    def visit_MatMult(self, node: ast.MatMult) -> _T_visit_return:
1✔
713
        """Allow multiplication (`@`)."""
714
        return self.node_contents_visit(node)
1✔
715

716
    def visit_BoolOp(self, node: ast.BoolOp) -> _T_visit_return:
1✔
717
        """Allow bool operator without restrictions."""
718
        return self.node_contents_visit(node)
1✔
719

720
    def visit_And(self, node: ast.And) -> _T_visit_return:
1✔
721
        """Allow bool operator `and` without restrictions."""
722
        return self.node_contents_visit(node)
1✔
723

724
    def visit_Or(self, node: ast.Or) -> _T_visit_return:
1✔
725
        """Allow bool operator `or` without restrictions."""
726
        return self.node_contents_visit(node)
1✔
727

728
    def visit_Compare(self, node: ast.Compare) -> _T_visit_return:
1✔
729
        """Allow comparison expressions without restrictions."""
730
        return self.node_contents_visit(node)
1✔
731

732
    def visit_Eq(self, node: ast.Eq) -> _T_visit_return:
1✔
733
        """Allow == expressions."""
734
        return self.node_contents_visit(node)
1✔
735

736
    def visit_NotEq(self, node: ast.NotEq) -> _T_visit_return:
1✔
737
        """Allow != expressions."""
738
        return self.node_contents_visit(node)
1✔
739

740
    def visit_Lt(self, node: ast.Lt) -> _T_visit_return:
1✔
741
        """Allow < expressions."""
742
        return self.node_contents_visit(node)
1✔
743

744
    def visit_LtE(self, node: ast.LtE) -> _T_visit_return:
1✔
745
        """Allow <= expressions."""
746
        return self.node_contents_visit(node)
1✔
747

748
    def visit_Gt(self, node: ast.Gt) -> _T_visit_return:
1✔
749
        """Allow > expressions."""
750
        return self.node_contents_visit(node)
1✔
751

752
    def visit_GtE(self, node: ast.GtE) -> _T_visit_return:
1✔
753
        """Allow >= expressions."""
754
        return self.node_contents_visit(node)
1✔
755

756
    def visit_Is(self, node: ast.Is) -> _T_visit_return:
1✔
757
        """Allow `is` expressions."""
758
        return self.node_contents_visit(node)
1✔
759

760
    def visit_IsNot(self, node: ast.IsNot) -> _T_visit_return:
1✔
761
        """Allow `is not` expressions."""
762
        return self.node_contents_visit(node)
1✔
763

764
    def visit_In(self, node: ast.In) -> _T_visit_return:
1✔
765
        """Allow `in` expressions."""
766
        return self.node_contents_visit(node)
1✔
767

768
    def visit_NotIn(self, node: ast.NotIn) -> _T_visit_return:
1✔
769
        """Allow `not in` expressions."""
770
        return self.node_contents_visit(node)
1✔
771

772
    def visit_Call(self, node: ast.Call) -> _T_visit_return:
1✔
773
        """Checks calls with '*args' and '**kwargs'.
774

775
        Note: The following happens only if '*args' or '**kwargs' is used.
776

777
        Transfroms 'foo(<all the possible ways of args>)' into
778
        _apply_(foo, <all the possible ways for args>)
779

780
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
781
        Python to collapse all the myriad ways to call functions
782
        into one manageable from.
783

784
        From there, '_apply_()' wraps args and kws in guarded accessors,
785
        then calls the function, returning the value.
786
        """
787

788
        if isinstance(node.func, ast.Name):
1✔
789
            if node.func.id == 'exec':
1✔
790
                self.error(node, 'Exec calls are not allowed.')
1✔
791
            elif node.func.id == 'eval':
1✔
792
                self.error(node, 'Eval calls are not allowed.')
1✔
793

794
        needs_wrap = False
1✔
795

796
        for pos_arg in node.args:
1✔
797
            if isinstance(pos_arg, ast.Starred):
1✔
798
                needs_wrap = True
1✔
799

800
        for keyword_arg in node.keywords:
1✔
801
            if keyword_arg.arg is None:
1✔
802
                needs_wrap = True
1✔
803

804
        node = self.node_contents_visit(node)
1✔
805

806
        if not needs_wrap:
1✔
807
            return node
1✔
808

809
        node.args.insert(0, node.func)
1✔
810
        node.func = ast.Name('_apply_', ast.Load())
1✔
811
        copy_locations(node.func, node.args[0])
1✔
812
        return node
1✔
813

814
    def visit_keyword(self, node: ast.keyword) -> _T_visit_return:
1✔
815
        """
816

817
        """
818
        return self.node_contents_visit(node)
1✔
819

820
    def visit_IfExp(self, node: ast.IfExp) -> _T_visit_return:
1✔
821
        """Allow `if` expressions without restrictions."""
822
        return self.node_contents_visit(node)
1✔
823

824
    def visit_Attribute(self, node: ast.Attribute) -> _T_visit_return:
1✔
825
        """Checks and mutates attribute access/assignment.
826

827
        'a.b' becomes '_getattr_(a, "b")'
828
        'a.b = c' becomes '_write_(a).b = c'
829
        'del a.b' becomes 'del _write_(a).b'
830

831
        The _write_ function should return a security proxy.
832
        """
833
        if node.attr.startswith('_') and node.attr != '_':
1✔
834
            self.error(
1✔
835
                node,
836
                '"{name}" is an invalid attribute name because it starts '
837
                'with "_".'.format(name=node.attr))
838

839
        if node.attr.endswith('__roles__'):
1✔
840
            self.error(
1✔
841
                node,
842
                '"{name}" is an invalid attribute name because it ends '
843
                'with "__roles__".'.format(name=node.attr))
844

845
        if node.attr in INSPECT_ATTRIBUTES:
1✔
846
            self.error(
1✔
847
                node,
848
                f'"{node.attr}" is a restricted name,'
849
                ' that is forbidden to access in RestrictedPython.',
850
            )
851

852
        if isinstance(node.ctx, ast.Load):
1✔
853
            node = self.node_contents_visit(node)
1✔
854
            new_node = ast.Call(
1✔
855
                func=ast.Name('_getattr_', ast.Load()),
856
                args=[node.value, ast.Constant(node.attr)],
857
                keywords=[])
858

859
            copy_locations(new_node, node)
1✔
860
            return new_node
1✔
861

862
        elif isinstance(node.ctx, (ast.Store, ast.Del)):
1✔
863
            node = self.node_contents_visit(node)
1✔
864
            new_value = ast.Call(
1✔
865
                func=ast.Name('_write_', ast.Load()),
866
                args=[node.value],
867
                keywords=[])
868

869
            copy_locations(new_value, node.value)
1✔
870
            node.value = new_value
1✔
871
            return node
1✔
872

873
        else:  # pragma: no cover
874
            # Impossible Case only ctx Load, Store and Del are defined in ast.
875
            raise NotImplementedError(
876
                f"Unknown ctx type: {type(node.ctx)}")
877

878
    # Subscripting
879

880
    def visit_Subscript(self, node: ast.Subscript) -> _T_visit_return:
1✔
881
        """Transforms all kinds of subscripts.
882

883
        'foo[bar]' becomes '_getitem_(foo, bar)'
884
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
885
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
886
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
887
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
888
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
889
        'foo[a] = c' becomes '_write_(foo)[a] = c'
890
        'del foo[a]' becomes 'del _write_(foo)[a]'
891

892
        The _write_ function should return a security proxy.
893
        """
894
        node = self.node_contents_visit(node)
1✔
895

896
        # 'AugStore' and 'AugLoad' are defined in 'Python.asdl' as possible
897
        # 'expr_context'. However, according to Python/ast.c
898
        # they are NOT used by the implementation => No need to worry here.
899
        # Instead ast.c creates 'AugAssign' nodes, which can be visited.
900

901
        if isinstance(node.ctx, ast.Load):
1✔
902
            new_node = ast.Call(
1✔
903
                func=ast.Name('_getitem_', ast.Load()),
904
                args=[node.value, node.slice],
905
                keywords=[])
906

907
            copy_locations(new_node, node)
1✔
908
            return new_node
1✔
909

910
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
911
            new_value = ast.Call(
1✔
912
                func=ast.Name('_write_', ast.Load()),
913
                args=[node.value],
914
                keywords=[])
915

916
            copy_locations(new_value, node)
1✔
917
            node.value = new_value
1✔
918
            return node
1✔
919

920
        else:  # pragma: no cover
921
            # Impossible Case only ctx Load, Store and Del are defined in ast.
922
            raise NotImplementedError(
923
                f"Unknown ctx type: {type(node.ctx)}")
924

925
    def visit_Slice(self, node: ast.Slice) -> _T_visit_return:
1✔
926
        """
927

928
        """
929
        return self.node_contents_visit(node)
1✔
930

931
    # Comprehensions
932

933
    def visit_ListComp(self, node: ast.ListComp) -> _T_visit_return:
1✔
934
        """
935

936
        """
937
        return self.node_contents_visit(node)
1✔
938

939
    def visit_SetComp(self, node: ast.SetComp) -> _T_visit_return:
1✔
940
        """
941

942
        """
943
        return self.node_contents_visit(node)
1✔
944

945
    def visit_GeneratorExp(self, node: ast.GeneratorExp) -> _T_visit_return:
1✔
946
        """
947

948
        """
949
        return self.node_contents_visit(node)
1✔
950

951
    def visit_DictComp(self, node: ast.DictComp) -> _T_visit_return:
1✔
952
        """
953

954
        """
955
        return self.node_contents_visit(node)
1✔
956

957
    def visit_comprehension(self, node: ast.comprehension) -> _T_visit_return:
1✔
958
        """
959

960
        """
961
        return self.guard_iter(node)
1✔
962

963
    # Statements
964

965
    def visit_Assign(self, node: ast.Assign) -> _T_visit_return:
1✔
966
        """
967

968
        """
969

970
        node = self.node_contents_visit(node)
1✔
971

972
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
973
            return node
1✔
974

975
        # Handle sequence unpacking.
976
        # For briefness this example omits cleanup of the temporary variables.
977
        # Check 'transform_tuple_assign' how its done.
978
        #
979
        # - Single target (with nested support)
980
        # (a, (b, (c, d))) = <exp>
981
        # is converted to
982
        # (a, t1) = _getiter_(<exp>)
983
        # (b, t2) = _getiter_(t1)
984
        # (c, d) = _getiter_(t2)
985
        #
986
        # - Multi targets
987
        # (a, b) = (c, d) = <exp>
988
        # is converted to
989
        # (c, d) = _getiter_(<exp>)
990
        # (a, b) = _getiter_(<exp>)
991
        # Why is this valid ? The original bytecode for this multi targets
992
        # behaves the same way.
993

994
        # ast.NodeTransformer works with list results.
995
        # He injects it at the right place of the node's parent statements.
996
        new_nodes = []
1✔
997

998
        # python fills the right most target first.
999
        for target in reversed(node.targets):
1✔
1000
            if isinstance(target, ast.Tuple):
1✔
1001
                wrapper = ast.Assign(
1✔
1002
                    targets=[target],
1003
                    value=self.protect_unpack_sequence(target, node.value))
1004
                new_nodes.append(wrapper)
1✔
1005
            else:
1006
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1007
                new_nodes.append(new_node)
1✔
1008

1009
        for new_node in new_nodes:
1✔
1010
            copy_locations(new_node, node)
1✔
1011

1012
        return new_nodes
1✔
1013

1014
    def visit_AugAssign(self, node: ast.AugAssign) -> _T_visit_return:
1✔
1015
        """Forbid certain kinds of AugAssign
1016

1017
        According to the language reference (and ast.c) the following nodes
1018
        are are possible:
1019
        Name, Attribute, Subscript
1020

1021
        Note that although augmented assignment of attributes and
1022
        subscripts is disallowed, augmented assignment of names (such
1023
        as 'n += 1') is allowed.
1024
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1025
        """
1026

1027
        node = self.node_contents_visit(node)
1✔
1028

1029
        if isinstance(node.target, ast.Attribute):
1✔
1030
            self.error(
1✔
1031
                node,
1032
                "Augmented assignment of attributes is not allowed.")
1033
            return node
1✔
1034

1035
        elif isinstance(node.target, ast.Subscript):
1✔
1036
            self.error(
1✔
1037
                node,
1038
                "Augmented assignment of object items "
1039
                "and slices is not allowed.")
1040
            return node
1✔
1041

1042
        elif isinstance(node.target, ast.Name):
1✔
1043
            new_node = ast.Assign(
1✔
1044
                targets=[node.target],
1045
                value=ast.Call(
1046
                    func=ast.Name('_inplacevar_', ast.Load()),
1047
                    args=[
1048
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1049
                        ast.Name(node.target.id, ast.Load()),
1050
                        node.value
1051
                    ],
1052
                    keywords=[]))
1053

1054
            copy_locations(new_node, node)
1✔
1055
            return new_node
1✔
1056
        else:  # pragma: no cover
1057
            # Impossible Case - Only Node Types:
1058
            # * Name
1059
            # * Attribute
1060
            # * Subscript
1061
            # defined, those are checked before.
1062
            raise NotImplementedError(
1063
                f"Unknown target type: {type(node.target)}")
1064

1065
    def visit_Raise(self, node: ast.Raise) -> _T_visit_return:
1✔
1066
        """Allow `raise` statements without restrictions."""
1067
        return self.node_contents_visit(node)
1✔
1068

1069
    def visit_Assert(self, node: ast.Assert) -> _T_visit_return:
1✔
1070
        """Allow assert statements without restrictions."""
1071
        return self.node_contents_visit(node)
1✔
1072

1073
    def visit_Delete(self, node: ast.Delete) -> _T_visit_return:
1✔
1074
        """Allow `del` statements without restrictions."""
1075
        return self.node_contents_visit(node)
1✔
1076

1077
    def visit_Pass(self, node: ast.Pass) -> _T_visit_return:
1✔
1078
        """Allow `pass` statements without restrictions."""
1079
        return self.node_contents_visit(node)
1✔
1080

1081
    # Imports
1082

1083
    def visit_Import(self, node: ast.Import) -> _T_visit_return:
1✔
1084
        """Allow `import` statements with restrictions.
1085
        See check_import_names."""
1086
        return self.check_import_names(node)
1✔
1087

1088
    def visit_ImportFrom(self, node: ast.ImportFrom) -> _T_visit_return:
1✔
1089
        """Allow `import from` statements with restrictions.
1090
        See check_import_names."""
1091
        return self.check_import_names(node)
1✔
1092

1093
    def visit_alias(self, node: ast.alias) -> _T_visit_return:
1✔
1094
        """Allow `as` statements in import and import from statements."""
1095
        return self.node_contents_visit(node)
1✔
1096

1097
    # Control flow
1098

1099
    def visit_If(self, node: ast.If) -> _T_visit_return:
1✔
1100
        """Allow `if` statements without restrictions."""
1101
        return self.node_contents_visit(node)
1✔
1102

1103
    def visit_For(self, node: ast.For) -> _T_visit_return:
1✔
1104
        """Allow `for` statements with some restrictions."""
1105
        return self.guard_iter(node)
1✔
1106

1107
    def visit_While(self, node: ast.While) -> _T_visit_return:
1✔
1108
        """Allow `while` statements."""
1109
        return self.node_contents_visit(node)
1✔
1110

1111
    def visit_Break(self, node: ast.Break) -> _T_visit_return:
1✔
1112
        """Allow `break` statements without restrictions."""
1113
        return self.node_contents_visit(node)
1✔
1114

1115
    def visit_Continue(self, node: ast.Continue) -> _T_visit_return:
1✔
1116
        """Allow `continue` statements without restrictions."""
1117
        return self.node_contents_visit(node)
1✔
1118

1119
    def visit_Try(self, node: ast.Try) -> _T_visit_return:
1✔
1120
        """Allow `try` without restrictions."""
1121
        return self.node_contents_visit(node)
1✔
1122

1123
    def visit_TryStar(self, node: ast.AST) -> _T_visit_return:
1✔
1124
        """Disallow `ExceptionGroup` due to a potential sandbox escape.
1125

1126
        TODO: Type Annotation for node when dropping support
1127
              for Python < 3.11 should be ast.TryStar.
1128
        """
1129
        self.not_allowed(node)
1✔
1130

1131
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> _T_visit_return:
1✔
1132
        """Protect exception handlers."""
1133
        node = self.node_contents_visit(node)
1✔
1134
        self.check_name(node, node.name)
1✔
1135
        return node
1✔
1136

1137
    def visit_With(self, node: ast.With) -> _T_visit_return:
1✔
1138
        """Protect tuple unpacking on with statements."""
1139
        node = self.node_contents_visit(node)
1✔
1140

1141
        for item in reversed(node.items):
1✔
1142
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1143
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1144
                    node,
1145
                    item.optional_vars)
1146

1147
                item.optional_vars = tmp_target
1✔
1148
                node.body.insert(0, unpack)
1✔
1149

1150
        return node
1✔
1151

1152
    def visit_withitem(self, node: ast.withitem) -> _T_visit_return:
1✔
1153
        """Allow `with` statements (context managers) without restrictions."""
1154
        return self.node_contents_visit(node)
1✔
1155

1156
    # Function and class definitions
1157

1158
    def visit_FunctionDef(self, node: ast.FunctionDef) -> _T_visit_return:
1✔
1159
        """Allow function definitions (`def`) with some restrictions."""
1160
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1161
        self.check_function_argument_names(node)
1✔
1162

1163
        with self.print_info.new_print_scope():
1✔
1164
            node = self.node_contents_visit(node)
1✔
1165
            self.inject_print_collector(node)
1✔
1166
        return node
1✔
1167

1168
    def visit_Lambda(self, node: ast.Lambda) -> _T_visit_return:
1✔
1169
        """Allow lambda with some restrictions."""
1170
        self.check_function_argument_names(node)
1✔
1171
        return self.node_contents_visit(node)
1✔
1172

1173
    def visit_arguments(self, node: ast.arguments) -> _T_visit_return:
1✔
1174
        """
1175

1176
        """
1177
        return self.node_contents_visit(node)
1✔
1178

1179
    def visit_arg(self, node: ast.arg) -> _T_visit_return:
1✔
1180
        """
1181

1182
        """
1183
        return self.node_contents_visit(node)
1✔
1184

1185
    def visit_Return(self, node: ast.Return) -> _T_visit_return:
1✔
1186
        """Allow `return` statements without restrictions."""
1187
        return self.node_contents_visit(node)
1✔
1188

1189
    def visit_Yield(self, node: ast.Yield) -> _T_visit_return:
1✔
1190
        """Allow `yield`statements without restrictions."""
1191
        return self.node_contents_visit(node)
1✔
1192

1193
    def visit_YieldFrom(self, node: ast.YieldFrom) -> _T_visit_return:
1✔
1194
        """Allow `yield`statements without restrictions."""
1195
        return self.node_contents_visit(node)
1✔
1196

1197
    def visit_Global(self, node: ast.Global) -> _T_visit_return:
1✔
1198
        """Allow `global` statements without restrictions."""
1199
        return self.node_contents_visit(node)
1✔
1200

1201
    def visit_Nonlocal(self, node: ast.Nonlocal) -> _T_visit_return:
1✔
1202
        """Deny `nonlocal` statements."""
1203
        self.not_allowed(node)
1✔
1204

1205
    def visit_ClassDef(self, node: ast.ClassDef) -> _T_visit_return:
1✔
1206
        """Check the name of a class definition."""
1207
        self.check_name(node, node.name)
1✔
1208
        node = self.node_contents_visit(node)
1✔
1209
        if any(keyword.arg == 'metaclass' for keyword in node.keywords):
1✔
1210
            self.error(
1✔
1211
                node, 'The keyword argument "metaclass" is not allowed.')
1212
        CLASS_DEF = textwrap.dedent('''\
1✔
1213
            class {0.name}(metaclass=__metaclass__):
1214
                pass
1215
        '''.format(node))
1216
        new_class_node = ast.parse(CLASS_DEF).body[0]
1✔
1217
        new_class_node.body = node.body
1✔
1218
        new_class_node.bases = node.bases
1✔
1219
        new_class_node.decorator_list = node.decorator_list
1✔
1220
        return new_class_node
1✔
1221

1222
    def visit_Module(self, node: ast.Module) -> _T_visit_return:
1✔
1223
        """Add the print_collector (only if print is used) at the top."""
1224
        node = self.node_contents_visit(node)
1✔
1225

1226
        # Inject the print collector after 'from __future__ import ....'
1227
        position = 0
1✔
1228
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1229
            if not isinstance(child, ast.ImportFrom):
1✔
1230
                break
1✔
1231

1232
            if not child.module == '__future__':
1✔
1233
                break
1✔
1234

1235
        self.inject_print_collector(node, position)
1✔
1236
        return node
1✔
1237

1238
    # Async und await
1239

1240
    def visit_AsyncFunctionDef(
1✔
1241
            self, node: ast.AsyncFunctionDef) -> _T_visit_return:
1242
        """Deny async functions."""
1243
        self.not_allowed(node)
1✔
1244

1245
    def visit_Await(self, node: ast.Await) -> _T_visit_return:
1✔
1246
        """Deny async functionality."""
1247
        self.not_allowed(node)
1✔
1248

1249
    def visit_AsyncFor(self, node: ast.AsyncFor) -> _T_visit_return:
1✔
1250
        """Deny async functionality."""
1251
        self.not_allowed(node)
1✔
1252

1253
    def visit_AsyncWith(self, node: ast.AsyncWith) -> _T_visit_return:
1✔
1254
        """Deny async functionality."""
1255
        self.not_allowed(node)
1✔
1256

1257
    # Assignment expressions (walrus operator ``:=``)
1258
    # New in 3.8
1259
    def visit_NamedExpr(self, node: ast.NamedExpr) -> _T_visit_return:
1✔
1260
        """Allow assignment expressions under some circumstances."""
1261
        # while the grammar requires ``node.target`` to be a ``Name``
1262
        # the abstract syntax is more permissive and allows an ``expr``.
1263
        # We support only a ``Name``.
1264
        # This is safe as the expression can only add/modify local
1265
        # variables. While this may hide global variables, an
1266
        # (implicitly performed) name check guarantees (as usual)
1267
        # that no essential global variable is hidden.
1268
        node = self.node_contents_visit(node)  # this checks ``node.target``
1✔
1269
        target = node.target
1✔
1270
        if not isinstance(target, ast.Name):
1✔
1271
            self.error(
1✔
1272
                node,
1273
                "Assignment expressions are only allowed for simple targets")
1274
        return node
1✔
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

© 2026 Coveralls, Inc