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

zopefoundation / RestrictedPython / 26814889682

02 Jun 2026 10:49AM UTC coverage: 98.98% (+0.008%) from 98.972%
26814889682

Pull #317

github

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

207 of 209 branches covered (99.04%)

157 of 159 new or added lines in 7 files covered. (98.74%)

2 existing lines in 2 files now uncovered.

2523 of 2549 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

25

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

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

56

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

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

114

115
# When new ast nodes are generated they have no 'lineno', 'end_lineno',
116
# 'col_offset' and 'end_col_offset'. This function copies these fields from the
117
# incoming node:
118
def copy_locations(new_node: ast.AST, old_node: ast.AST) -> None:
1✔
119
    assert 'lineno' in new_node._attributes
1✔
120
    new_node.lineno = old_node.lineno
1✔
121

122
    assert 'end_lineno' in new_node._attributes
1✔
123
    new_node.end_lineno = old_node.end_lineno
1✔
124

125
    assert 'col_offset' in new_node._attributes
1✔
126
    new_node.col_offset = old_node.col_offset
1✔
127

128
    assert 'end_col_offset' in new_node._attributes
1✔
129
    new_node.end_col_offset = old_node.end_col_offset
1✔
130

131
    ast.fix_missing_locations(new_node)
1✔
132

133

134
class PrintInfo:
1✔
135
    def __init__(self):
1✔
136
        self.print_used = False
1✔
137
        self.printed_used = False
1✔
138

139
    @contextlib.contextmanager
1✔
140
    def new_print_scope(self):
1✔
141
        old_print_used = self.print_used
1✔
142
        old_printed_used = self.printed_used
1✔
143

144
        self.print_used = False
1✔
145
        self.printed_used = False
1✔
146

147
        try:
1✔
148
            yield
1✔
149
        finally:
150
            self.print_used = old_print_used
1✔
151
            self.printed_used = old_printed_used
1✔
152

153

154
class RestrictingNodeTransformer(ast.NodeTransformer):
1✔
155
    errors: list[str]
1✔
156
    warnings: list[str]
1✔
157
    used_names: dict[str, bool]
1✔
158

159
    def __init__(self,
1✔
160
                 errors: list[str] | None = None,
161
                 warnings: list[str] | None = None,
162
                 used_names: dict[str, bool] | None = None):
163
        super().__init__()
1✔
164
        self.errors = [] if errors is None else errors
1✔
165
        self.warnings = [] if warnings is None else warnings
1✔
166

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

174
        # Global counter to construct temporary variable names.
175
        self._tmp_idx = 0
1✔
176

177
        self.print_info = PrintInfo()
1✔
178

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

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

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

198
    def guard_iter(self, node: ast.AST) -> ast.AST:
1✔
199
        """
200
        Converts:
201
            for x in expr
202
        to
203
            for x in _getiter_(expr)
204

205
        Also used for
206
        * list comprehensions
207
        * dict comprehensions
208
        * set comprehensions
209
        * generator expresions
210
        """
211
        node = self.node_contents_visit(node)
1✔
212

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

225
        copy_locations(new_iter, node.iter)
1✔
226
        node.iter = new_iter
1✔
227
        return node
1✔
228

229
    def is_starred(self, ob: ast.AST) -> bool:
1✔
230
        return isinstance(ob, ast.Starred)
1✔
231

232
    def gen_unpack_spec(self, tpl: ast.Tuple) -> ast.Dict:
1✔
233
        """Generate a specification for 'guarded_unpack_sequence'.
234

235
        This spec is used to protect sequence unpacking.
236
        The primary goal of this spec is to tell which elements in a sequence
237
        are sequences again. These 'child' sequences have to be protected
238
        again.
239

240
        For example there is a sequence like this:
241
            (a, (b, c), (d, (e, f))) = g
242

243
        On a higher level the spec says:
244
            - There is a sequence of len 3
245
            - The element at index 1 is a sequence again with len 2
246
            - The element at index 2 is a sequence again with len 2
247
              - The element at index 1 in this subsequence is a sequence again
248
                with len 2
249

250
        With this spec 'guarded_unpack_sequence' does something like this for
251
        protection (len checks are omitted):
252

253
            t = list(_getiter_(g))
254
            t[1] = list(_getiter_(t[1]))
255
            t[2] = list(_getiter_(t[2]))
256
            t[2][1] = list(_getiter_(t[2][1]))
257
            return t
258

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

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

279
        spec.keys.append(ast.Constant('childs'))
1✔
280
        spec.values.append(ast.Tuple([], ast.Load()))
1✔
281

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

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

297
            elif isinstance(val, ast.Tuple):
1✔
298
                el = ast.Tuple([], ast.Load())
1✔
299
                el.elts.append(ast.Constant(idx - offset))
1✔
300
                el.elts.append(self.gen_unpack_spec(val))
1✔
301
                spec.values[0].elts.append(el)
1✔
302

303
        spec.keys.append(ast.Constant('min_len'))
1✔
304
        spec.values.append(ast.Constant(min_len))
1✔
305

306
        return spec
1✔
307

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

318
    def gen_unpack_wrapper(self, node: ast.AST,
1✔
319
                           target: ast.Tuple) -> tuple[ast.Name, ast.Try]:
320
        """Helper function to protect tuple unpacks.
321

322
        node: used to copy the locations for the new nodes.
323
        target: is the tuple which must be protected.
324

325
        It returns a tuple with two element.
326

327
        Element 1: Is a temporary name node which must be used to
328
                   replace the target.
329
                   The context (store, param) is defined
330
                   by the 'ctx' parameter..
331

332
        Element 2: Is a try .. finally where the body performs the
333
                   protected tuple unpack of the temporary variable
334
                   into the original target.
335
        """
336

337
        # Generate a tmp name to replace the tuple with.
338
        tmp_name = self.gen_tmp_name()
1✔
339

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

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

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

363
        copy_locations(tmp_target, node)
1✔
364
        copy_locations(cleanup, node)
1✔
365

366
        return (tmp_target, cleanup)
1✔
367

368
    def gen_none_node(self) -> ast.Constant:
1✔
369
        return ast.Constant(None)
1✔
370

371
    def gen_del_stmt(self, name_to_del: str) -> ast.Delete:
1✔
372
        return ast.Delete(targets=[ast.Name(name_to_del, ast.Del())])
1✔
373

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

381
        If ``allow_magic_methods is True`` names in `ALLOWED_FUNC_NAMES`
382
        are additionally allowed although their names start with `_`.
383

384
        """
385
        if name is None:
1✔
386
            return
1✔
387

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

403
    def check_function_argument_names(self, node: ast.FunctionDef) -> None:
1✔
404
        for arg in node.args.args:
1✔
405
            self.check_name(node, arg.arg)
1✔
406

407
        if node.args.vararg:
1✔
408
            self.check_name(node, node.args.vararg.arg)
1✔
409

410
        if node.args.kwarg:
1✔
411
            self.check_name(node, node.args.kwarg.arg)
1✔
412

413
        for arg in node.args.kwonlyargs:
1✔
414
            self.check_name(node, arg.arg)
1✔
415

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

419
        This is a protection against rebinding dunder names like
420
        _getitem_, _write_ via imports.
421

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

431
        return self.node_contents_visit(node)
1✔
432

433
    def inject_print_collector(self, node: ast.AST, position: int = 0) -> None:
1✔
434
        print_used = self.print_info.print_used
1✔
435
        printed_used = self.print_info.printed_used
1✔
436

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

447
            if isinstance(node, ast.Module):
1✔
448
                _print.lineno = position
1✔
449
                _print.col_offset = position
1✔
450
                _print.end_lineno = position
1✔
451
                _print.end_col_offset = position
1✔
452
                ast.fix_missing_locations(_print)
1✔
453
            else:
454
                copy_locations(_print, node)
1✔
455

456
            node.body.insert(position, _print)
1✔
457

458
            if not printed_used:
1✔
459
                self.warn(node, "Prints, but never reads 'printed' variable.")
1✔
460

461
            elif not print_used:
1✔
462
                self.warn(node, "Doesn't print, but reads 'printed' variable.")
1✔
463

464
    # Special Functions for an ast.NodeTransformer
465

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

469
        This is needed to prevent new ast nodes from new Python versions to be
470
        trusted before any security review.
471

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

481
    def not_allowed(self, node: ast.AST) -> None:
1✔
482
        self.error(
1✔
483
            node,
484
            f'{node.__class__.__name__} statements are not allowed.')
485

486
    def node_contents_visit(self, node: ast.AST) -> ast.AST:
1✔
487
        """Visit the contents of a node."""
488
        return super().generic_visit(node)
1✔
489

490
    # ast for Literals
491

492
    def visit_Constant(self, node: ast.Constant) -> ast.AST | None:
1✔
493
        """Allow constant literals.
494

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

501
    def visit_Interactive(self, node: ast.Interactive) -> ast.AST | None:
1✔
502
        """Allow single mode without restrictions."""
503
        return self.node_contents_visit(node)
1✔
504

505
    def visit_List(self, node: ast.List) -> ast.AST | None:
1✔
506
        """Allow list literals without restrictions."""
507
        return self.node_contents_visit(node)
1✔
508

509
    def visit_Tuple(self, node: ast.Tuple) -> ast.AST | None:
1✔
510
        """Allow tuple literals without restrictions."""
511
        return self.node_contents_visit(node)
1✔
512

513
    def visit_Set(self, node: ast.Set) -> ast.AST | None:
1✔
514
        """Allow set literals without restrictions."""
515
        return self.node_contents_visit(node)
1✔
516

517
    def visit_Dict(self, node: ast.Dict) -> ast.AST | None:
1✔
518
        """Allow dict literals without restrictions."""
519
        return self.node_contents_visit(node)
1✔
520

521
    def visit_FormattedValue(self, node: ast.FormattedValue) -> ast.AST | None:
1✔
522
        """Allow f-strings without restrictions."""
523
        return self.node_contents_visit(node)
1✔
524

525
    def visit_TemplateStr(self, node: ast.AST) -> ast.AST | None:
1✔
526
        """Template strings are allowed by default.
527

528
        As Template strings are a very basic template mechanism, that needs
529
        additional rendering logic to be useful, they are not blocked by
530
        default.
531
        Those rendering logic would be affected by RestrictedPython as well.
532

533
        TODO: Change Type Annotation to ast.TemplateStr when
534
              Support for Python 3.13 is dropped.
535
        """
NEW
536
        return self.node_contents_visit(node)
×
537

538
    def visit_Interpolation(self, node: ast.AST) -> ast.AST | None:
1✔
539
        """Interpolations are allowed by default.
540

541
        As Interpolations are part of Template Strings, they are needed
542
        to be reached in the context of RestrictedPython as Template Strings
543
        are allowed. As a user has to provide additional rendering logic
544
        to make use of Template Strings, the security implications of
545
        Interpolations are limited in the context of RestrictedPython.
546

547
        TODO: Change Type Annotation to ast.Interpolation when
548
              Support for Python 3.13 is dropped.
549
        """
NEW
550
        return self.node_contents_visit(node)
×
551

552
    def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.AST | None:
1✔
553
        """Allow joined string without restrictions."""
554
        return self.node_contents_visit(node)
1✔
555

556
    # ast for Variables
557

558
    def visit_Name(self, node: ast.Name) -> ast.Name | None:
1✔
559
        """Prevents access to protected names.
560

561
        Converts use of the name 'printed' to this expression: '_print()'
562
        """
563

564
        node = self.node_contents_visit(node)
1✔
565

566
        if isinstance(node.ctx, ast.Load):
1✔
567
            if node.id == 'printed':
1✔
568
                self.print_info.printed_used = True
1✔
569
                new_node = ast.Call(
1✔
570
                    func=ast.Name("_print", ast.Load()),
571
                    args=[],
572
                    keywords=[])
573

574
                copy_locations(new_node, node)
1✔
575
                return new_node
1✔
576

577
            elif node.id == 'print':
1✔
578
                self.print_info.print_used = True
1✔
579
                new_node = ast.Attribute(
1✔
580
                    value=ast.Name('_print', ast.Load()),
581
                    attr="_call_print",
582
                    ctx=ast.Load())
583

584
                copy_locations(new_node, node)
1✔
585
                return new_node
1✔
586

587
            self.used_names[node.id] = True
1✔
588

589
        self.check_name(node, node.id)
1✔
590
        return node
1✔
591

592
    def visit_Load(self, node: ast.Load) -> ast.Load | None:
1✔
593
        """
594

595
        """
596
        return self.node_contents_visit(node)
1✔
597

598
    def visit_Store(self, node: ast.Store) -> ast.AST | None:
1✔
599
        """
600

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

604
    def visit_Del(self, node: ast.Del) -> ast.AST | None:
1✔
605
        """
606

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

610
    def visit_Starred(self, node: ast.Starred) -> ast.AST | None:
1✔
611
        """
612

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

616
    # Expressions
617

618
    def visit_Expression(self, node: ast.Expression) -> ast.AST | None:
1✔
619
        """Allow Expression statements without restrictions.
620

621
        They are in the AST when using the `eval` compile mode.
622
        """
623
        return self.node_contents_visit(node)
1✔
624

625
    def visit_Expr(self, node: ast.Expr) -> ast.AST | None:
1✔
626
        """Allow Expr statements (any expression) without restrictions."""
627
        return self.node_contents_visit(node)
1✔
628

629
    def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST | None:
1✔
630
        """
631
        UnaryOp (Unary Operations) is the overall element for:
632
        * Not --> which should be allowed
633
        * UAdd --> Positive notation of variables (e.g. +var)
634
        * USub --> Negative notation of variables (e.g. -var)
635
        """
636
        return self.node_contents_visit(node)
1✔
637

638
    def visit_UAdd(self, node: ast.UAdd) -> ast.AST | None:
1✔
639
        """Allow positive notation of variables. (e.g. +var)"""
640
        return self.node_contents_visit(node)
1✔
641

642
    def visit_USub(self, node: ast.USub) -> ast.AST | None:
1✔
643
        """Allow negative notation of variables. (e.g. -var)"""
644
        return self.node_contents_visit(node)
1✔
645

646
    def visit_Not(self, node: ast.Not) -> ast.AST | None:
1✔
647
        """Allow the `not` operator."""
648
        return self.node_contents_visit(node)
1✔
649

650
    def visit_Invert(self, node: ast.Invert) -> ast.AST | None:
1✔
651
        """Allow `~` expressions."""
652
        return self.node_contents_visit(node)
1✔
653

654
    def visit_BinOp(self, node: ast.BinOp) -> ast.AST | None:
1✔
655
        """Allow binary operations."""
656
        return self.node_contents_visit(node)
1✔
657

658
    def visit_Add(self, node: ast.Add) -> ast.AST | None:
1✔
659
        """Allow `+` expressions."""
660
        return self.node_contents_visit(node)
1✔
661

662
    def visit_Sub(self, node: ast.Sub) -> ast.AST | None:
1✔
663
        """Allow `-` expressions."""
664
        return self.node_contents_visit(node)
1✔
665

666
    def visit_Mult(self, node: ast.Mult) -> ast.AST | None:
1✔
667
        """Allow `*` expressions."""
668
        return self.node_contents_visit(node)
1✔
669

670
    def visit_Div(self, node: ast.Div) -> ast.AST | None:
1✔
671
        """Allow `/` expressions."""
672
        return self.node_contents_visit(node)
1✔
673

674
    def visit_FloorDiv(self, node: ast.FloorDiv) -> ast.AST | None:
1✔
675
        """Allow `//` expressions."""
676
        return self.node_contents_visit(node)
1✔
677

678
    def visit_Mod(self, node: ast.Mod) -> ast.AST | None:
1✔
679
        """Allow `%` expressions."""
680
        return self.node_contents_visit(node)
1✔
681

682
    def visit_Pow(self, node: ast.Pow) -> ast.AST | None:
1✔
683
        """Allow `**` expressions."""
684
        return self.node_contents_visit(node)
1✔
685

686
    def visit_LShift(self, node: ast.LShift) -> ast.AST | None:
1✔
687
        """Allow `<<` expressions."""
688
        return self.node_contents_visit(node)
1✔
689

690
    def visit_RShift(self, node: ast.RShift) -> ast.AST | None:
1✔
691
        """Allow `>>` expressions."""
692
        return self.node_contents_visit(node)
1✔
693

694
    def visit_BitOr(self, node: ast.BitOr) -> ast.AST | None:
1✔
695
        """Allow `|` expressions."""
696
        return self.node_contents_visit(node)
1✔
697

698
    def visit_BitXor(self, node: ast.BitXor) -> ast.AST | None:
1✔
699
        """Allow `^` expressions."""
700
        return self.node_contents_visit(node)
1✔
701

702
    def visit_BitAnd(self, node: ast.BitAnd) -> ast.AST | None:
1✔
703
        """Allow `&` expressions."""
704
        return self.node_contents_visit(node)
1✔
705

706
    def visit_MatMult(self, node: ast.MatMult) -> ast.AST | None:
1✔
707
        """Allow multiplication (`@`)."""
708
        return self.node_contents_visit(node)
1✔
709

710
    def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST | None:
1✔
711
        """Allow bool operator without restrictions."""
712
        return self.node_contents_visit(node)
1✔
713

714
    def visit_And(self, node: ast.And) -> ast.AST | None:
1✔
715
        """Allow bool operator `and` without restrictions."""
716
        return self.node_contents_visit(node)
1✔
717

718
    def visit_Or(self, node: ast.Or) -> ast.AST | None:
1✔
719
        """Allow bool operator `or` without restrictions."""
720
        return self.node_contents_visit(node)
1✔
721

722
    def visit_Compare(self, node: ast.Compare) -> ast.AST | None:
1✔
723
        """Allow comparison expressions without restrictions."""
724
        return self.node_contents_visit(node)
1✔
725

726
    def visit_Eq(self, node: ast.Eq) -> ast.AST | None:
1✔
727
        """Allow == expressions."""
728
        return self.node_contents_visit(node)
1✔
729

730
    def visit_NotEq(self, node: ast.NotEq) -> ast.AST | None:
1✔
731
        """Allow != expressions."""
732
        return self.node_contents_visit(node)
1✔
733

734
    def visit_Lt(self, node: ast.Lt) -> ast.AST | None:
1✔
735
        """Allow < expressions."""
736
        return self.node_contents_visit(node)
1✔
737

738
    def visit_LtE(self, node: ast.LtE) -> ast.AST | None:
1✔
739
        """Allow <= expressions."""
740
        return self.node_contents_visit(node)
1✔
741

742
    def visit_Gt(self, node: ast.Gt) -> ast.AST | None:
1✔
743
        """Allow > expressions."""
744
        return self.node_contents_visit(node)
1✔
745

746
    def visit_GtE(self, node: ast.GtE) -> ast.AST | None:
1✔
747
        """Allow >= expressions."""
748
        return self.node_contents_visit(node)
1✔
749

750
    def visit_Is(self, node: ast.Is) -> ast.AST | None:
1✔
751
        """Allow `is` expressions."""
752
        return self.node_contents_visit(node)
1✔
753

754
    def visit_IsNot(self, node: ast.IsNot) -> ast.AST | None:
1✔
755
        """Allow `is not` expressions."""
756
        return self.node_contents_visit(node)
1✔
757

758
    def visit_In(self, node: ast.In) -> ast.AST | None:
1✔
759
        """Allow `in` expressions."""
760
        return self.node_contents_visit(node)
1✔
761

762
    def visit_NotIn(self, node: ast.NotIn) -> ast.AST | None:
1✔
763
        """Allow `not in` expressions."""
764
        return self.node_contents_visit(node)
1✔
765

766
    def visit_Call(self, node: ast.Call) -> ast.AST | None:
1✔
767
        """Checks calls with '*args' and '**kwargs'.
768

769
        Note: The following happens only if '*args' or '**kwargs' is used.
770

771
        Transfroms 'foo(<all the possible ways of args>)' into
772
        _apply_(foo, <all the possible ways for args>)
773

774
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
775
        Python to collapse all the myriad ways to call functions
776
        into one manageable from.
777

778
        From there, '_apply_()' wraps args and kws in guarded accessors,
779
        then calls the function, returning the value.
780
        """
781

782
        if isinstance(node.func, ast.Name):
1✔
783
            if node.func.id == 'exec':
1✔
784
                self.error(node, 'Exec calls are not allowed.')
1✔
785
            elif node.func.id == 'eval':
1✔
786
                self.error(node, 'Eval calls are not allowed.')
1✔
787

788
        needs_wrap = False
1✔
789

790
        for pos_arg in node.args:
1✔
791
            if isinstance(pos_arg, ast.Starred):
1✔
792
                needs_wrap = True
1✔
793

794
        for keyword_arg in node.keywords:
1✔
795
            if keyword_arg.arg is None:
1✔
796
                needs_wrap = True
1✔
797

798
        node = self.node_contents_visit(node)
1✔
799

800
        if not needs_wrap:
1✔
801
            return node
1✔
802

803
        node.args.insert(0, node.func)
1✔
804
        node.func = ast.Name('_apply_', ast.Load())
1✔
805
        copy_locations(node.func, node.args[0])
1✔
806
        return node
1✔
807

808
    def visit_keyword(self, node: ast.keyword) -> ast.AST | None:
1✔
809
        """
810

811
        """
812
        return self.node_contents_visit(node)
1✔
813

814
    def visit_IfExp(self, node: ast.IfExp) -> ast.AST | None:
1✔
815
        """Allow `if` expressions without restrictions."""
816
        return self.node_contents_visit(node)
1✔
817

818
    def visit_Attribute(self, node: ast.Attribute) -> ast.AST | None:
1✔
819
        """Checks and mutates attribute access/assignment.
820

821
        'a.b' becomes '_getattr_(a, "b")'
822
        'a.b = c' becomes '_write_(a).b = c'
823
        'del a.b' becomes 'del _write_(a).b'
824

825
        The _write_ function should return a security proxy.
826
        """
827
        if node.attr.startswith('_') and node.attr != '_':
1✔
828
            self.error(
1✔
829
                node,
830
                '"{name}" is an invalid attribute name because it starts '
831
                'with "_".'.format(name=node.attr))
832

833
        if node.attr.endswith('__roles__'):
1✔
834
            self.error(
1✔
835
                node,
836
                '"{name}" is an invalid attribute name because it ends '
837
                'with "__roles__".'.format(name=node.attr))
838

839
        if node.attr in INSPECT_ATTRIBUTES:
1✔
840
            self.error(
1✔
841
                node,
842
                f'"{node.attr}" is a restricted name,'
843
                ' that is forbidden to access in RestrictedPython.',
844
            )
845

846
        if isinstance(node.ctx, ast.Load):
1✔
847
            node = self.node_contents_visit(node)
1✔
848
            new_node = ast.Call(
1✔
849
                func=ast.Name('_getattr_', ast.Load()),
850
                args=[node.value, ast.Constant(node.attr)],
851
                keywords=[])
852

853
            copy_locations(new_node, node)
1✔
854
            return new_node
1✔
855

856
        elif isinstance(node.ctx, (ast.Store, ast.Del)):
1✔
857
            node = self.node_contents_visit(node)
1✔
858
            new_value = ast.Call(
1✔
859
                func=ast.Name('_write_', ast.Load()),
860
                args=[node.value],
861
                keywords=[])
862

863
            copy_locations(new_value, node.value)
1✔
864
            node.value = new_value
1✔
865
            return node
1✔
866

867
        else:  # pragma: no cover
868
            # Impossible Case only ctx Load, Store and Del are defined in ast.
869
            raise NotImplementedError(
870
                f"Unknown ctx type: {type(node.ctx)}")
871

872
    # Subscripting
873

874
    def visit_Subscript(self, node: ast.Subscript) -> ast.AST | None:
1✔
875
        """Transforms all kinds of subscripts.
876

877
        'foo[bar]' becomes '_getitem_(foo, bar)'
878
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
879
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
880
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
881
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
882
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
883
        'foo[a] = c' becomes '_write_(foo)[a] = c'
884
        'del foo[a]' becomes 'del _write_(foo)[a]'
885

886
        The _write_ function should return a security proxy.
887
        """
888
        node = self.node_contents_visit(node)
1✔
889

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

895
        if isinstance(node.ctx, ast.Load):
1✔
896
            new_node = ast.Call(
1✔
897
                func=ast.Name('_getitem_', ast.Load()),
898
                args=[node.value, node.slice],
899
                keywords=[])
900

901
            copy_locations(new_node, node)
1✔
902
            return new_node
1✔
903

904
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
905
            new_value = ast.Call(
1✔
906
                func=ast.Name('_write_', ast.Load()),
907
                args=[node.value],
908
                keywords=[])
909

910
            copy_locations(new_value, node)
1✔
911
            node.value = new_value
1✔
912
            return node
1✔
913

914
        else:  # pragma: no cover
915
            # Impossible Case only ctx Load, Store and Del are defined in ast.
916
            raise NotImplementedError(
917
                f"Unknown ctx type: {type(node.ctx)}")
918

919
    def visit_Slice(self, node: ast.Slice) -> ast.AST | None:
1✔
920
        """
921

922
        """
923
        return self.node_contents_visit(node)
1✔
924

925
    # Comprehensions
926

927
    def visit_ListComp(self, node: ast.ListComp) -> ast.AST | None:
1✔
928
        """
929

930
        """
931
        return self.node_contents_visit(node)
1✔
932

933
    def visit_SetComp(self, node: ast.SetComp) -> ast.AST | None:
1✔
934
        """
935

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

939
    def visit_GeneratorExp(self, node: ast.GeneratorExp) -> ast.AST | None:
1✔
940
        """
941

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

945
    def visit_DictComp(self, node: ast.DictComp) -> ast.AST | None:
1✔
946
        """
947

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

951
    def visit_comprehension(self, node: ast.comprehension) -> ast.AST | None:
1✔
952
        """
953

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

957
    # Statements
958

959
    def visit_Assign(self, node: ast.Assign) -> ast.AST | None:
1✔
960
        """
961

962
        """
963

964
        node = self.node_contents_visit(node)
1✔
965

966
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
967
            return node
1✔
968

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

988
        # ast.NodeTransformer works with list results.
989
        # He injects it at the right place of the node's parent statements.
990
        new_nodes = []
1✔
991

992
        # python fills the right most target first.
993
        for target in reversed(node.targets):
1✔
994
            if isinstance(target, ast.Tuple):
1✔
995
                wrapper = ast.Assign(
1✔
996
                    targets=[target],
997
                    value=self.protect_unpack_sequence(target, node.value))
998
                new_nodes.append(wrapper)
1✔
999
            else:
1000
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1001
                new_nodes.append(new_node)
1✔
1002

1003
        for new_node in new_nodes:
1✔
1004
            copy_locations(new_node, node)
1✔
1005

1006
        return new_nodes
1✔
1007

1008
    def visit_AugAssign(self, node: ast.AugAssign) -> ast.AST | None:
1✔
1009
        """Forbid certain kinds of AugAssign
1010

1011
        According to the language reference (and ast.c) the following nodes
1012
        are are possible:
1013
        Name, Attribute, Subscript
1014

1015
        Note that although augmented assignment of attributes and
1016
        subscripts is disallowed, augmented assignment of names (such
1017
        as 'n += 1') is allowed.
1018
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1019
        """
1020

1021
        node = self.node_contents_visit(node)
1✔
1022

1023
        if isinstance(node.target, ast.Attribute):
1✔
1024
            self.error(
1✔
1025
                node,
1026
                "Augmented assignment of attributes is not allowed.")
1027
            return node
1✔
1028

1029
        elif isinstance(node.target, ast.Subscript):
1✔
1030
            self.error(
1✔
1031
                node,
1032
                "Augmented assignment of object items "
1033
                "and slices is not allowed.")
1034
            return node
1✔
1035

1036
        elif isinstance(node.target, ast.Name):
1✔
1037
            new_node = ast.Assign(
1✔
1038
                targets=[node.target],
1039
                value=ast.Call(
1040
                    func=ast.Name('_inplacevar_', ast.Load()),
1041
                    args=[
1042
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1043
                        ast.Name(node.target.id, ast.Load()),
1044
                        node.value
1045
                    ],
1046
                    keywords=[]))
1047

1048
            copy_locations(new_node, node)
1✔
1049
            return new_node
1✔
1050
        else:  # pragma: no cover
1051
            # Impossible Case - Only Node Types:
1052
            # * Name
1053
            # * Attribute
1054
            # * Subscript
1055
            # defined, those are checked before.
1056
            raise NotImplementedError(
1057
                f"Unknown target type: {type(node.target)}")
1058

1059
    def visit_Raise(self, node: ast.Raise) -> ast.AST | None:
1✔
1060
        """Allow `raise` statements without restrictions."""
1061
        return self.node_contents_visit(node)
1✔
1062

1063
    def visit_Assert(self, node: ast.Assert) -> ast.AST | None:
1✔
1064
        """Allow assert statements without restrictions."""
1065
        return self.node_contents_visit(node)
1✔
1066

1067
    def visit_Delete(self, node: ast.Delete) -> ast.AST | None:
1✔
1068
        """Allow `del` statements without restrictions."""
1069
        return self.node_contents_visit(node)
1✔
1070

1071
    def visit_Pass(self, node: ast.Pass) -> ast.AST | None:
1✔
1072
        """Allow `pass` statements without restrictions."""
1073
        return self.node_contents_visit(node)
1✔
1074

1075
    # Imports
1076

1077
    def visit_Import(self, node: ast.Import) -> ast.AST | None:
1✔
1078
        """Allow `import` statements with restrictions.
1079
        See check_import_names."""
1080
        return self.check_import_names(node)
1✔
1081

1082
    def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST | None:
1✔
1083
        """Allow `import from` statements with restrictions.
1084
        See check_import_names."""
1085
        return self.check_import_names(node)
1✔
1086

1087
    def visit_alias(self, node: ast.alias) -> ast.AST | None:
1✔
1088
        """Allow `as` statements in import and import from statements."""
1089
        return self.node_contents_visit(node)
1✔
1090

1091
    # Control flow
1092

1093
    def visit_If(self, node: ast.If) -> ast.AST | None:
1✔
1094
        """Allow `if` statements without restrictions."""
1095
        return self.node_contents_visit(node)
1✔
1096

1097
    def visit_For(self, node: ast.For) -> ast.AST | None:
1✔
1098
        """Allow `for` statements with some restrictions."""
1099
        return self.guard_iter(node)
1✔
1100

1101
    def visit_While(self, node: ast.While) -> ast.AST | None:
1✔
1102
        """Allow `while` statements."""
1103
        return self.node_contents_visit(node)
1✔
1104

1105
    def visit_Break(self, node: ast.Break) -> ast.AST | None:
1✔
1106
        """Allow `break` statements without restrictions."""
1107
        return self.node_contents_visit(node)
1✔
1108

1109
    def visit_Continue(self, node: ast.Continue) -> ast.AST | None:
1✔
1110
        """Allow `continue` statements without restrictions."""
1111
        return self.node_contents_visit(node)
1✔
1112

1113
    def visit_Try(self, node: ast.Try) -> ast.AST | None:
1✔
1114
        """Allow `try` without restrictions."""
1115
        return self.node_contents_visit(node)
1✔
1116

1117
    def visit_TryStar(self, node: ast.AST) -> ast.AST | None:
1✔
1118
        """Disallow `ExceptionGroup` due to a potential sandbox escape.
1119

1120
        TODO: Type Annotation for node when dropping support
1121
              for Python < 3.11 should be ast.TryStar.
1122
        """
1123
        self.not_allowed(node)
1✔
1124

1125
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> ast.AST | None:
1✔
1126
        """Protect exception handlers."""
1127
        node = self.node_contents_visit(node)
1✔
1128
        self.check_name(node, node.name)
1✔
1129
        return node
1✔
1130

1131
    def visit_With(self, node: ast.With) -> ast.AST | None:
1✔
1132
        """Protect tuple unpacking on with statements."""
1133
        node = self.node_contents_visit(node)
1✔
1134

1135
        for item in reversed(node.items):
1✔
1136
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1137
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1138
                    node,
1139
                    item.optional_vars)
1140

1141
                item.optional_vars = tmp_target
1✔
1142
                node.body.insert(0, unpack)
1✔
1143

1144
        return node
1✔
1145

1146
    def visit_withitem(self, node: ast.withitem) -> ast.AST | None:
1✔
1147
        """Allow `with` statements (context managers) without restrictions."""
1148
        return self.node_contents_visit(node)
1✔
1149

1150
    # Function and class definitions
1151

1152
    def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST | None:
1✔
1153
        """Allow function definitions (`def`) with some restrictions."""
1154
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1155
        self.check_function_argument_names(node)
1✔
1156

1157
        with self.print_info.new_print_scope():
1✔
1158
            node = self.node_contents_visit(node)
1✔
1159
            self.inject_print_collector(node)
1✔
1160
        return node
1✔
1161

1162
    def visit_Lambda(self, node: ast.Lambda) -> ast.AST | None:
1✔
1163
        """Allow lambda with some restrictions."""
1164
        self.check_function_argument_names(node)
1✔
1165
        return self.node_contents_visit(node)
1✔
1166

1167
    def visit_arguments(self, node: ast.arguments) -> ast.AST | None:
1✔
1168
        """
1169

1170
        """
1171
        return self.node_contents_visit(node)
1✔
1172

1173
    def visit_arg(self, node: ast.arg) -> ast.AST | None:
1✔
1174
        """
1175

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

1179
    def visit_Return(self, node: ast.Return) -> ast.AST | None:
1✔
1180
        """Allow `return` statements without restrictions."""
1181
        return self.node_contents_visit(node)
1✔
1182

1183
    def visit_Yield(self, node: ast.Yield) -> ast.AST | None:
1✔
1184
        """Allow `yield`statements without restrictions."""
1185
        return self.node_contents_visit(node)
1✔
1186

1187
    def visit_YieldFrom(self, node: ast.YieldFrom) -> ast.AST | None:
1✔
1188
        """Allow `yield`statements without restrictions."""
1189
        return self.node_contents_visit(node)
1✔
1190

1191
    def visit_Global(self, node: ast.Global) -> ast.AST | None:
1✔
1192
        """Allow `global` statements without restrictions."""
1193
        return self.node_contents_visit(node)
1✔
1194

1195
    def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.AST | None:
1✔
1196
        """Deny `nonlocal` statements."""
1197
        self.not_allowed(node)
1✔
1198

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

1216
    def visit_Module(self, node: ast.Module) -> ast.AST | None:
1✔
1217
        """Add the print_collector (only if print is used) at the top."""
1218
        node = self.node_contents_visit(node)
1✔
1219

1220
        # Inject the print collector after 'from __future__ import ....'
1221
        position = 0
1✔
1222
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1223
            if not isinstance(child, ast.ImportFrom):
1✔
1224
                break
1✔
1225

1226
            if not child.module == '__future__':
1✔
1227
                break
1✔
1228

1229
        self.inject_print_collector(node, position)
1✔
1230
        return node
1✔
1231

1232
    # Async und await
1233

1234
    def visit_AsyncFunctionDef(
1✔
1235
            self, node: ast.AsyncFunctionDef) -> ast.AST | None:
1236
        """Deny async functions."""
1237
        self.not_allowed(node)
1✔
1238

1239
    def visit_Await(self, node: ast.Await) -> ast.AST | None:
1✔
1240
        """Deny async functionality."""
1241
        self.not_allowed(node)
1✔
1242

1243
    def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AST | None:
1✔
1244
        """Deny async functionality."""
1245
        self.not_allowed(node)
1✔
1246

1247
    def visit_AsyncWith(self, node: ast.AsyncWith) -> ast.AST | None:
1✔
1248
        """Deny async functionality."""
1249
        self.not_allowed(node)
1✔
1250

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