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

zopefoundation / RestrictedPython / 18617084392

18 Oct 2025 02:45PM UTC coverage: 97.903% (-0.9%) from 98.772%
18617084392

Pull #303

github

loechel
fix coverage numbers
Pull Request #303: Type Annotations for RestrictedPython

214 of 233 branches covered (91.85%)

147 of 171 new or added lines in 5 files covered. (85.96%)

5 existing lines in 2 files now uncovered.

2521 of 2575 relevant lines covered (97.9%)

0.98 hits per line

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

94.79
/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_globals",
77
    # "f_lasti",  # int
78
    # "f_lineno",  # int
79
    "f_locals",
80
    "f_trace",
81
    # on code objects:
82
    # "co_argcount",  # int
83
    "co_code",
84
    # "co_cellvars",  # tuple of str
85
    # "co_consts",   # tuple of str
86
    # "co_filename",  # str
87
    # "co_firstlineno",  # int
88
    # "co_flags",  # int
89
    # "co_lnotab",  # mapping between ints and indices
90
    # "co_freevars",  # tuple of strings
91
    # "co_posonlyargcount",  # int
92
    # "co_kwonlyargcount",  # int
93
    # "co_name",  # str
94
    # "co_qualname",  # str
95
    # "co_names",  # str
96
    # "co_nlocals",  # int
97
    # "co_stacksize",  # int
98
    # "co_varnames",  # tuple of str
99
    # on generator objects:
100
    "gi_frame",
101
    # "gi_running",  # bool
102
    "gi_code",
103
    "gi_yieldfrom",
104
    # on coroutine objects:
105
    "cr_await",
106
    "cr_frame",
107
    # "cr_running",  # bool
108
    "cr_code",
109
    "cr_origin",
110
])
111

112

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

120
    assert 'end_lineno' in new_node._attributes
1✔
121
    new_node.end_lineno = old_node.end_lineno
1✔
122

123
    assert 'col_offset' in new_node._attributes
1✔
124
    new_node.col_offset = old_node.col_offset
1✔
125

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

129
    ast.fix_missing_locations(new_node)
1✔
130

131

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

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

142
        self.print_used = False
1✔
143
        self.printed_used = False
1✔
144

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

151

152
class RestrictingNodeTransformer(ast.NodeTransformer):
1✔
153

154
    def __init__(self,
1✔
155
                 errors: list[str] | None = None,
156
                 warnings: list[str] | None = None,
157
                 used_names: dict[str,
158
                                  str] | None = None):
159
        super().__init__()
1✔
160
        self.errors = [] if errors is None else errors
1✔
161
        self.warnings = [] if warnings is None else warnings
1✔
162

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

170
        # Global counter to construct temporary variable names.
171
        self._tmp_idx = 0
1✔
172

173
        self.print_info = PrintInfo()
1✔
174

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

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

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

194
    def guard_iter(self, node: ast.AST) -> ast.AST:
1✔
195
        """
196
        Converts:
197
            for x in expr
198
        to
199
            for x in _getiter_(expr)
200

201
        Also used for
202
        * list comprehensions
203
        * dict comprehensions
204
        * set comprehensions
205
        * generator expresions
206
        """
207
        node = self.node_contents_visit(node)
1✔
208

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

221
        copy_locations(new_iter, node.iter)
1✔
222
        node.iter = new_iter
1✔
223
        return node
1✔
224

225
    def is_starred(self, ob: ast.AST) -> bool:
1✔
226
        return isinstance(ob, ast.Starred)
1✔
227

228
    def gen_unpack_spec(self, tpl: ast.Tuple) -> ast.Dict:
1✔
229
        """Generate a specification for 'guarded_unpack_sequence'.
230

231
        This spec is used to protect sequence unpacking.
232
        The primary goal of this spec is to tell which elements in a sequence
233
        are sequences again. These 'child' sequences have to be protected
234
        again.
235

236
        For example there is a sequence like this:
237
            (a, (b, c), (d, (e, f))) = g
238

239
        On a higher level the spec says:
240
            - There is a sequence of len 3
241
            - The element at index 1 is a sequence again with len 2
242
            - The element at index 2 is a sequence again with len 2
243
              - The element at index 1 in this subsequence is a sequence again
244
                with len 2
245

246
        With this spec 'guarded_unpack_sequence' does something like this for
247
        protection (len checks are omitted):
248

249
            t = list(_getiter_(g))
250
            t[1] = list(_getiter_(t[1]))
251
            t[2] = list(_getiter_(t[2]))
252
            t[2][1] = list(_getiter_(t[2][1]))
253
            return t
254

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

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

275
        spec.keys.append(ast.Constant('childs'))
1✔
276
        spec.values.append(ast.Tuple([], ast.Load()))
1✔
277

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

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

293
            elif isinstance(val, ast.Tuple):
1✔
294
                el = ast.Tuple([], ast.Load())
1✔
295
                el.elts.append(ast.Constant(idx - offset))
1✔
296
                el.elts.append(self.gen_unpack_spec(val))
1✔
297
                spec.values[0].elts.append(el)
1✔
298

299
        spec.keys.append(ast.Constant('min_len'))
1✔
300
        spec.values.append(ast.Constant(min_len))
1✔
301

302
        return spec
1✔
303

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

314
    def gen_unpack_wrapper(self, node: ast.AST,
1✔
315
                           target: ast.Tuple) -> tuple[ast.Name, ast.Try]:
316
        """Helper function to protect tuple unpacks.
317

318
        node: used to copy the locations for the new nodes.
319
        target: is the tuple which must be protected.
320

321
        It returns a tuple with two element.
322

323
        Element 1: Is a temporary name node which must be used to
324
                   replace the target.
325
                   The context (store, param) is defined
326
                   by the 'ctx' parameter..
327

328
        Element 2: Is a try .. finally where the body performs the
329
                   protected tuple unpack of the temporary variable
330
                   into the original target.
331
        """
332

333
        # Generate a tmp name to replace the tuple with.
334
        tmp_name = self.gen_tmp_name()
1✔
335

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

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

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

359
        copy_locations(tmp_target, node)
1✔
360
        copy_locations(cleanup, node)
1✔
361

362
        return (tmp_target, cleanup)
1✔
363

364
    def gen_none_node(self) -> ast.NameConstant:
1✔
365
        return ast.NameConstant(value=None)
×
366

367
    def gen_del_stmt(self, name_to_del: str) -> ast.Delete:
1✔
368
        return ast.Delete(targets=[ast.Name(name_to_del, ast.Del())])
1✔
369

370
    def transform_slice(self, slice_: ast.AST) -> ast.AST:
1✔
371
        """Transform slices into function parameters.
372

373
        ast.Slice nodes are only allowed within a ast.Subscript node.
374
        To use a slice as an argument of ast.Call it has to be converted.
375
        Conversion is done by calling the 'slice' function from builtins
376
        """
377

378
        if isinstance(slice_, ast.expr):
1!
379
            # Python 3.9+
380
            return slice_
1✔
381

382
        elif isinstance(slice_, ast.Index):
×
383
            return slice_.value
×
384

385
        elif isinstance(slice_, ast.Slice):
×
386
            # Create a python slice object.
387
            args = []
×
388

389
            if slice_.lower:
×
390
                args.append(slice_.lower)
×
391
            else:
392
                args.append(self.gen_none_node())
×
393

394
            if slice_.upper:
×
395
                args.append(slice_.upper)
×
396
            else:
397
                args.append(self.gen_none_node())
×
398

399
            if slice_.step:
×
400
                args.append(slice_.step)
×
401
            else:
402
                args.append(self.gen_none_node())
×
403

404
            return ast.Call(
×
405
                func=ast.Name('slice', ast.Load()),
406
                args=args,
407
                keywords=[])
408

NEW
409
        elif isinstance(slice_, (ast.Tuple, ast.ExtSlice)):
×
410
            dims = ast.Tuple([], ast.Load())
×
411
            for item in slice_.dims:
×
412
                dims.elts.append(self.transform_slice(item))
×
413
            return dims
×
414

415
        else:  # pragma: no cover
416
            # Index, Slice and ExtSlice are only defined Slice types.
417
            raise NotImplementedError(f"Unknown slice type: {slice_}")
418

419
    def check_name(
1✔
420
            self,
421
            node: ast.AST,
422
            name: str,
423
            allow_magic_methods: bool = False) -> None:
424
        """Check names if they are allowed.
425

426
        If ``allow_magic_methods is True`` names in `ALLOWED_FUNC_NAMES`
427
        are additionally allowed although their names start with `_`.
428

429
        """
430
        if name is None:
1✔
431
            return
1✔
432

433
        if (name.startswith('_')
1✔
434
                and name != '_'
435
                and not (allow_magic_methods
436
                         and name in ALLOWED_FUNC_NAMES
437
                         and node.col_offset != 0)):
438
            self.error(
1✔
439
                node,
440
                '"{name}" is an invalid variable name because it '
441
                'starts with "_"'.format(name=name))
442
        elif name.endswith('__roles__'):
1✔
443
            self.error(node, '"%s" is an invalid variable name because '
1✔
444
                       'it ends with "__roles__".' % name)
445
        elif name in FORBIDDEN_FUNC_NAMES:
1✔
446
            self.error(node, f'"{name}" is a reserved name.')
1✔
447

448
    def check_function_argument_names(self, node: ast.FunctionDef) -> None:
1✔
449
        for arg in node.args.args:
1✔
450
            self.check_name(node, arg.arg)
1✔
451

452
        if node.args.vararg:
1✔
453
            self.check_name(node, node.args.vararg.arg)
1✔
454

455
        if node.args.kwarg:
1✔
456
            self.check_name(node, node.args.kwarg.arg)
1✔
457

458
        for arg in node.args.kwonlyargs:
1✔
459
            self.check_name(node, arg.arg)
1✔
460

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

464
        This is a protection against rebinding dunder names like
465
        _getitem_, _write_ via imports.
466

467
        => 'from _a import x' is ok, because '_a' is not added to the scope.
468
        """
469
        for name in node.names:
1✔
470
            if '*' in name.name:
1✔
471
                self.error(node, '"*" imports are not allowed.')
1✔
472
            self.check_name(node, name.name)
1✔
473
            if name.asname:
1✔
474
                self.check_name(node, name.asname)
1✔
475

476
        return self.node_contents_visit(node)
1✔
477

478
    def inject_print_collector(self, node: ast.AST, position: int = 0) -> None:
1✔
479
        print_used = self.print_info.print_used
1✔
480
        printed_used = self.print_info.printed_used
1✔
481

482
        if print_used or printed_used:
1✔
483
            # Add '_print = _print_(_getattr_)' add the top of a
484
            # function/module.
485
            _print = ast.Assign(
1✔
486
                targets=[ast.Name('_print', ast.Store())],
487
                value=ast.Call(
488
                    func=ast.Name("_print_", ast.Load()),
489
                    args=[ast.Name("_getattr_", ast.Load())],
490
                    keywords=[]))
491

492
            if isinstance(node, ast.Module):
1✔
493
                _print.lineno = position
1✔
494
                _print.col_offset = position
1✔
495
                _print.end_lineno = position
1✔
496
                _print.end_col_offset = position
1✔
497
                ast.fix_missing_locations(_print)
1✔
498
            else:
499
                copy_locations(_print, node)
1✔
500

501
            node.body.insert(position, _print)
1✔
502

503
            if not printed_used:
1✔
504
                self.warn(node, "Prints, but never reads 'printed' variable.")
1✔
505

506
            elif not print_used:
1✔
507
                self.warn(node, "Doesn't print, but reads 'printed' variable.")
1✔
508

509
    # Special Functions for an ast.NodeTransformer
510

511
    def generic_visit(self, node: ast.AST) -> ast.AST:
1✔
512
        """Reject ast nodes which do not have a corresponding `visit_` method.
513

514
        This is needed to prevent new ast nodes from new Python versions to be
515
        trusted before any security review.
516

517
        To access `generic_visit` on the super class use `node_contents_visit`.
518
        """
519
        self.warn(
1✔
520
            node,
521
            '{0.__class__.__name__}'
522
            ' statement is not known to RestrictedPython'.format(node)
523
        )
524
        self.not_allowed(node)
1✔
525

526
    def not_allowed(self, node: ast.AST) -> None:
1✔
527
        self.error(
1✔
528
            node,
529
            f'{node.__class__.__name__} statements are not allowed.')
530

531
    def node_contents_visit(self, node: ast.AST) -> ast.AST:
1✔
532
        """Visit the contents of a node."""
533
        return super().generic_visit(node)
1✔
534

535
    # ast for Literals
536

537
    def visit_Constant(self, node: ast.Constant) -> ast.Constant | None:
1✔
538
        """Allow constant literals with restriction for Ellipsis.
539

540
        Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in
541
        Python 3.8+.
542
        :see: https://docs.python.org/dev/whatsnew/3.8.html#deprecated
543
        """
544
        if node.value is Ellipsis:
1✔
545
            # Deny using `...`.
546
            # Special handling necessary as ``self.not_allowed(node)``
547
            # would return the Error Message:
548
            # 'Constant statements are not allowed.'
549
            # which is only partial true.
550
            self.error(node, 'Ellipsis statements are not allowed.')
1✔
551
            return
1✔
552
        return self.node_contents_visit(node)
1✔
553

554
    def visit_Interactive(self, node: ast.Interactive) -> ast.AST:
1✔
555
        """Allow single mode without restrictions."""
556
        return self.node_contents_visit(node)
1✔
557

558
    def visit_List(self, node: ast.List) -> ast.AST:
1✔
559
        """Allow list literals without restrictions."""
560
        return self.node_contents_visit(node)
1✔
561

562
    def visit_Tuple(self, node: ast.Tuple) -> ast.AST:
1✔
563
        """Allow tuple literals without restrictions."""
564
        return self.node_contents_visit(node)
1✔
565

566
    def visit_Set(self, node: ast.Set) -> ast.AST:
1✔
567
        """Allow set literals without restrictions."""
568
        return self.node_contents_visit(node)
1✔
569

570
    def visit_Dict(self, node: ast.Dict) -> ast.AST:
1✔
571
        """Allow dict literals without restrictions."""
572
        return self.node_contents_visit(node)
1✔
573

574
    def visit_FormattedValue(self, node: ast.FormattedValue) -> ast.AST:
1✔
575
        """Allow f-strings without restrictions."""
576
        return self.node_contents_visit(node)
1✔
577

578
    def visit_TemplateStr(self, node: ast.AST) -> ast.AST:
1✔
579
        """Template strings are allowed by default.
580

581
        As Template strings are a very basic template mechanism, that needs
582
        additional rendering logic to be useful, they are not blocked by
583
        default.
584
        Those rendering logic would be affected by RestrictedPython as well.
585

586
        TODO: Change Type Annotation to ast.TemplateStr when
587
              Support for Python 3.13 is dropped.
588
        """
NEW
589
        self.warn(
×
590
            node,
591
            'TemplateStr statements are not yet allowed, '
592
            'please use f-strings or a real template engine instead.')
593
        # self.not_allowed(node)
NEW
UNCOV
594
        return self.node_contents_visit(node)
×
595

596
    def visit_Interpolation(self, node: ast.AST) -> ast.AST:
1✔
597
        """Interpolations are allowed by default.
598
        As Interpolations are part of Template Strings, they are needed
599
        to be reached in the context of RestrictedPython as Template Strings
600
        are allowed. As a user has to provide additional rendering logic
601
        to make use of Template Strings, the security implications of
602
        Interpolations are limited in the context of RestrictedPython.
603

604
        TODO: Change Type Annotation to ast.Interpolation when
605
              Support for Python 3.13 is dropped.
606
        """
607
        # self.not_allowed(node)
NEW
UNCOV
608
        return self.node_contents_visit(node)
×
609

610
    def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.AST:
1✔
611
        """Allow joined string without restrictions."""
612
        return self.node_contents_visit(node)
1✔
613

614
    # ast for Variables
615

616
    def visit_Name(self, node: ast.Name) -> ast.Name | None:
1✔
617
        """Prevents access to protected names.
618

619
        Converts use of the name 'printed' to this expression: '_print()'
620
        """
621

622
        node = self.node_contents_visit(node)
1✔
623

624
        if isinstance(node.ctx, ast.Load):
1✔
625
            if node.id == 'printed':
1✔
626
                self.print_info.printed_used = True
1✔
627
                new_node = ast.Call(
1✔
628
                    func=ast.Name("_print", ast.Load()),
629
                    args=[],
630
                    keywords=[])
631

632
                copy_locations(new_node, node)
1✔
633
                return new_node
1✔
634

635
            elif node.id == 'print':
1✔
636
                self.print_info.print_used = True
1✔
637
                new_node = ast.Attribute(
1✔
638
                    value=ast.Name('_print', ast.Load()),
639
                    attr="_call_print",
640
                    ctx=ast.Load())
641

642
                copy_locations(new_node, node)
1✔
643
                return new_node
1✔
644

645
            self.used_names[node.id] = True
1✔
646

647
        self.check_name(node, node.id)
1✔
648
        return node
1✔
649

650
    def visit_Load(self, node: ast.Load) -> ast.Load | None:
1✔
651
        """
652

653
        """
654
        return self.node_contents_visit(node)
1✔
655

656
    def visit_Store(self, node: ast.Store) -> ast.AST:
1✔
657
        """
658

659
        """
660
        return self.node_contents_visit(node)
1✔
661

662
    def visit_Del(self, node: ast.Del) -> ast.Del:
1✔
663
        """
664

665
        """
666
        return self.node_contents_visit(node)
1✔
667

668
    def visit_Starred(self, node: ast.Starred) -> ast.AST:
1✔
669
        """
670

671
        """
672
        return self.node_contents_visit(node)
1✔
673

674
    # Expressions
675

676
    def visit_Expression(self, node: ast.Expression) -> ast.AST:
1✔
677
        """Allow Expression statements without restrictions.
678

679
        They are in the AST when using the `eval` compile mode.
680
        """
681
        return self.node_contents_visit(node)
1✔
682

683
    def visit_Expr(self, node: ast.Expr) -> ast.AST:
1✔
684
        """Allow Expr statements (any expression) without restrictions."""
685
        return self.node_contents_visit(node)
1✔
686

687
    def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST:
1✔
688
        """
689
        UnaryOp (Unary Operations) is the overall element for:
690
        * Not --> which should be allowed
691
        * UAdd --> Positive notation of variables (e.g. +var)
692
        * USub --> Negative notation of variables (e.g. -var)
693
        """
694
        return self.node_contents_visit(node)
1✔
695

696
    def visit_UAdd(self, node: ast.UAdd) -> ast.AST:
1✔
697
        """Allow positive notation of variables. (e.g. +var)"""
698
        return self.node_contents_visit(node)
1✔
699

700
    def visit_USub(self, node: ast.USub) -> ast.AST:
1✔
701
        """Allow negative notation of variables. (e.g. -var)"""
702
        return self.node_contents_visit(node)
1✔
703

704
    def visit_Not(self, node: ast.Not) -> ast.AST:
1✔
705
        """Allow the `not` operator."""
706
        return self.node_contents_visit(node)
1✔
707

708
    def visit_Invert(self, node: ast.Invert) -> ast.AST:
1✔
709
        """Allow `~` expressions."""
710
        return self.node_contents_visit(node)
1✔
711

712
    def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
1✔
713
        """Allow binary operations."""
714
        return self.node_contents_visit(node)
1✔
715

716
    def visit_Add(self, node: ast.Add) -> ast.AST:
1✔
717
        """Allow `+` expressions."""
718
        return self.node_contents_visit(node)
1✔
719

720
    def visit_Sub(self, node: ast.Sub) -> ast.AST:
1✔
721
        """Allow `-` expressions."""
722
        return self.node_contents_visit(node)
1✔
723

724
    def visit_Mult(self, node: ast.Mult) -> ast.AST:
1✔
725
        """Allow `*` expressions."""
726
        return self.node_contents_visit(node)
1✔
727

728
    def visit_Div(self, node: ast.Div) -> ast.AST:
1✔
729
        """Allow `/` expressions."""
730
        return self.node_contents_visit(node)
1✔
731

732
    def visit_FloorDiv(self, node: ast.FloorDiv) -> ast.AST:
1✔
733
        """Allow `//` expressions."""
734
        return self.node_contents_visit(node)
1✔
735

736
    def visit_Mod(self, node: ast.Mod) -> ast.AST:
1✔
737
        """Allow `%` expressions."""
738
        return self.node_contents_visit(node)
1✔
739

740
    def visit_Pow(self, node: ast.Pow) -> ast.AST:
1✔
741
        """Allow `**` expressions."""
742
        return self.node_contents_visit(node)
1✔
743

744
    def visit_LShift(self, node: ast.LShift) -> ast.AST:
1✔
745
        """Allow `<<` expressions."""
746
        return self.node_contents_visit(node)
1✔
747

748
    def visit_RShift(self, node: ast.RShift) -> ast.AST:
1✔
749
        """Allow `>>` expressions."""
750
        return self.node_contents_visit(node)
1✔
751

752
    def visit_BitOr(self, node: ast.BitOr) -> ast.AST:
1✔
753
        """Allow `|` expressions."""
754
        return self.node_contents_visit(node)
1✔
755

756
    def visit_BitXor(self, node: ast.BitXor) -> ast.AST:
1✔
757
        """Allow `^` expressions."""
758
        return self.node_contents_visit(node)
1✔
759

760
    def visit_BitAnd(self, node: ast.BitAnd) -> ast.AST:
1✔
761
        """Allow `&` expressions."""
762
        return self.node_contents_visit(node)
1✔
763

764
    def visit_MatMult(self, node: ast.MatMult) -> ast.AST:
1✔
765
        """Allow multiplication (`@`)."""
766
        return self.node_contents_visit(node)
1✔
767

768
    def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST:
1✔
769
        """Allow bool operator without restrictions."""
770
        return self.node_contents_visit(node)
1✔
771

772
    def visit_And(self, node: ast.And) -> ast.AST:
1✔
773
        """Allow bool operator `and` without restrictions."""
774
        return self.node_contents_visit(node)
1✔
775

776
    def visit_Or(self, node: ast.Or) -> ast.AST:
1✔
777
        """Allow bool operator `or` without restrictions."""
778
        return self.node_contents_visit(node)
1✔
779

780
    def visit_Compare(self, node: ast.Compare) -> ast.AST:
1✔
781
        """Allow comparison expressions without restrictions."""
782
        return self.node_contents_visit(node)
1✔
783

784
    def visit_Eq(self, node: ast.Eq) -> ast.AST:
1✔
785
        """Allow == expressions."""
786
        return self.node_contents_visit(node)
1✔
787

788
    def visit_NotEq(self, node: ast.NotEq) -> ast.AST:
1✔
789
        """Allow != expressions."""
790
        return self.node_contents_visit(node)
1✔
791

792
    def visit_Lt(self, node: ast.Lt) -> ast.AST:
1✔
793
        """Allow < expressions."""
794
        return self.node_contents_visit(node)
1✔
795

796
    def visit_LtE(self, node: ast.LtE) -> ast.AST:
1✔
797
        """Allow <= expressions."""
798
        return self.node_contents_visit(node)
1✔
799

800
    def visit_Gt(self, node: ast.Gt) -> ast.AST:
1✔
801
        """Allow > expressions."""
802
        return self.node_contents_visit(node)
1✔
803

804
    def visit_GtE(self, node: ast.GtE) -> ast.AST:
1✔
805
        """Allow >= expressions."""
806
        return self.node_contents_visit(node)
1✔
807

808
    def visit_Is(self, node: ast.Is) -> ast.AST:
1✔
809
        """Allow `is` expressions."""
810
        return self.node_contents_visit(node)
1✔
811

812
    def visit_IsNot(self, node: ast.IsNot) -> ast.AST:
1✔
813
        """Allow `is not` expressions."""
814
        return self.node_contents_visit(node)
1✔
815

816
    def visit_In(self, node: ast.In) -> ast.AST:
1✔
817
        """Allow `in` expressions."""
818
        return self.node_contents_visit(node)
1✔
819

820
    def visit_NotIn(self, node: ast.NotIn) -> ast.AST:
1✔
821
        """Allow `not in` expressions."""
822
        return self.node_contents_visit(node)
1✔
823

824
    def visit_Call(self, node: ast.Call) -> ast.AST:
1✔
825
        """Checks calls with '*args' and '**kwargs'.
826

827
        Note: The following happens only if '*args' or '**kwargs' is used.
828

829
        Transfroms 'foo(<all the possible ways of args>)' into
830
        _apply_(foo, <all the possible ways for args>)
831

832
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
833
        Python to collapse all the myriad ways to call functions
834
        into one manageable from.
835

836
        From there, '_apply_()' wraps args and kws in guarded accessors,
837
        then calls the function, returning the value.
838
        """
839

840
        if isinstance(node.func, ast.Name):
1✔
841
            if node.func.id == 'exec':
1✔
842
                self.error(node, 'Exec calls are not allowed.')
1✔
843
            elif node.func.id == 'eval':
1✔
844
                self.error(node, 'Eval calls are not allowed.')
1✔
845

846
        needs_wrap = False
1✔
847

848
        for pos_arg in node.args:
1✔
849
            if isinstance(pos_arg, ast.Starred):
1✔
850
                needs_wrap = True
1✔
851

852
        for keyword_arg in node.keywords:
1✔
853
            if keyword_arg.arg is None:
1✔
854
                needs_wrap = True
1✔
855

856
        node = self.node_contents_visit(node)
1✔
857

858
        if not needs_wrap:
1✔
859
            return node
1✔
860

861
        node.args.insert(0, node.func)
1✔
862
        node.func = ast.Name('_apply_', ast.Load())
1✔
863
        copy_locations(node.func, node.args[0])
1✔
864
        return node
1✔
865

866
    def visit_keyword(self, node: ast.keyword) -> ast.AST:
1✔
867
        """
868

869
        """
870
        return self.node_contents_visit(node)
1✔
871

872
    def visit_IfExp(self, node: ast.IfExp) -> ast.AST:
1✔
873
        """Allow `if` expressions without restrictions."""
874
        return self.node_contents_visit(node)
1✔
875

876
    def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
1✔
877
        """Checks and mutates attribute access/assignment.
878

879
        'a.b' becomes '_getattr_(a, "b")'
880
        'a.b = c' becomes '_write_(a).b = c'
881
        'del a.b' becomes 'del _write_(a).b'
882

883
        The _write_ function should return a security proxy.
884
        """
885
        if node.attr.startswith('_') and node.attr != '_':
1✔
886
            self.error(
1✔
887
                node,
888
                '"{name}" is an invalid attribute name because it starts '
889
                'with "_".'.format(name=node.attr))
890

891
        if node.attr.endswith('__roles__'):
1✔
892
            self.error(
1✔
893
                node,
894
                '"{name}" is an invalid attribute name because it ends '
895
                'with "__roles__".'.format(name=node.attr))
896

897
        if node.attr in INSPECT_ATTRIBUTES:
1✔
898
            self.error(
1✔
899
                node,
900
                f'"{node.attr}" is a restricted name,'
901
                ' that is forbidden to access in RestrictedPython.',
902
            )
903

904
        if isinstance(node.ctx, ast.Load):
1✔
905
            node = self.node_contents_visit(node)
1✔
906
            new_node = ast.Call(
1✔
907
                func=ast.Name('_getattr_', ast.Load()),
908
                args=[node.value, ast.Constant(node.attr)],
909
                keywords=[])
910

911
            copy_locations(new_node, node)
1✔
912
            return new_node
1✔
913

914
        elif isinstance(node.ctx, (ast.Store, ast.Del)):
1✔
915
            node = self.node_contents_visit(node)
1✔
916
            new_value = ast.Call(
1✔
917
                func=ast.Name('_write_', ast.Load()),
918
                args=[node.value],
919
                keywords=[])
920

921
            copy_locations(new_value, node.value)
1✔
922
            node.value = new_value
1✔
923
            return node
1✔
924

925
        else:  # pragma: no cover
926
            # Impossible Case only ctx Load, Store and Del are defined in ast.
927
            raise NotImplementedError(
928
                f"Unknown ctx type: {type(node.ctx)}")
929

930
    # Subscripting
931

932
    def visit_Subscript(self, node: ast.Subscript) -> ast.AST:
1✔
933
        """Transforms all kinds of subscripts.
934

935
        'foo[bar]' becomes '_getitem_(foo, bar)'
936
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
937
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
938
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
939
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
940
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
941
        'foo[a] = c' becomes '_write_(foo)[a] = c'
942
        'del foo[a]' becomes 'del _write_(foo)[a]'
943

944
        The _write_ function should return a security proxy.
945
        """
946
        node = self.node_contents_visit(node)
1✔
947

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

953
        if isinstance(node.ctx, ast.Load):
1✔
954
            new_node = ast.Call(
1✔
955
                func=ast.Name('_getitem_', ast.Load()),
956
                args=[node.value, self.transform_slice(node.slice)],
957
                keywords=[])
958

959
            copy_locations(new_node, node)
1✔
960
            return new_node
1✔
961

962
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
963
            new_value = ast.Call(
1✔
964
                func=ast.Name('_write_', ast.Load()),
965
                args=[node.value],
966
                keywords=[])
967

968
            copy_locations(new_value, node)
1✔
969
            node.value = new_value
1✔
970
            return node
1✔
971

972
        else:  # pragma: no cover
973
            # Impossible Case only ctx Load, Store and Del are defined in ast.
974
            raise NotImplementedError(
975
                f"Unknown ctx type: {type(node.ctx)}")
976

977
    def visit_Index(self, node: ast.Index) -> ast.AST:
1✔
978
        """
979

980
        """
UNCOV
981
        return self.node_contents_visit(node)
×
982

983
    def visit_Slice(self, node: ast.Slice) -> ast.AST:
1✔
984
        """
985

986
        """
987
        return self.node_contents_visit(node)
1✔
988

989
    def visit_ExtSlice(self, node: ast.ExtSlice) -> ast.AST:
1✔
990
        """
991

992
        """
UNCOV
993
        return self.node_contents_visit(node)
×
994

995
    # Comprehensions
996

997
    def visit_ListComp(self, node: ast.ListComp) -> ast.AST:
1✔
998
        """
999

1000
        """
1001
        return self.node_contents_visit(node)
1✔
1002

1003
    def visit_SetComp(self, node: ast.SetComp) -> ast.AST:
1✔
1004
        """
1005

1006
        """
1007
        return self.node_contents_visit(node)
1✔
1008

1009
    def visit_GeneratorExp(self, node: ast.GeneratorExp) -> ast.AST:
1✔
1010
        """
1011

1012
        """
1013
        return self.node_contents_visit(node)
1✔
1014

1015
    def visit_DictComp(self, node: ast.DictComp) -> ast.AST:
1✔
1016
        """
1017

1018
        """
1019
        return self.node_contents_visit(node)
1✔
1020

1021
    def visit_comprehension(self, node: ast.comprehension) -> ast.AST:
1✔
1022
        """
1023

1024
        """
1025
        return self.guard_iter(node)
1✔
1026

1027
    # Statements
1028

1029
    def visit_Assign(self, node: ast.Assign) -> ast.AST:
1✔
1030
        """
1031

1032
        """
1033

1034
        node = self.node_contents_visit(node)
1✔
1035

1036
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
1037
            return node
1✔
1038

1039
        # Handle sequence unpacking.
1040
        # For briefness this example omits cleanup of the temporary variables.
1041
        # Check 'transform_tuple_assign' how its done.
1042
        #
1043
        # - Single target (with nested support)
1044
        # (a, (b, (c, d))) = <exp>
1045
        # is converted to
1046
        # (a, t1) = _getiter_(<exp>)
1047
        # (b, t2) = _getiter_(t1)
1048
        # (c, d) = _getiter_(t2)
1049
        #
1050
        # - Multi targets
1051
        # (a, b) = (c, d) = <exp>
1052
        # is converted to
1053
        # (c, d) = _getiter_(<exp>)
1054
        # (a, b) = _getiter_(<exp>)
1055
        # Why is this valid ? The original bytecode for this multi targets
1056
        # behaves the same way.
1057

1058
        # ast.NodeTransformer works with list results.
1059
        # He injects it at the right place of the node's parent statements.
1060
        new_nodes = []
1✔
1061

1062
        # python fills the right most target first.
1063
        for target in reversed(node.targets):
1✔
1064
            if isinstance(target, ast.Tuple):
1✔
1065
                wrapper = ast.Assign(
1✔
1066
                    targets=[target],
1067
                    value=self.protect_unpack_sequence(target, node.value))
1068
                new_nodes.append(wrapper)
1✔
1069
            else:
1070
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1071
                new_nodes.append(new_node)
1✔
1072

1073
        for new_node in new_nodes:
1✔
1074
            copy_locations(new_node, node)
1✔
1075

1076
        return new_nodes
1✔
1077

1078
    def visit_AugAssign(self, node: ast.AugAssign) -> ast.AST:
1✔
1079
        """Forbid certain kinds of AugAssign
1080

1081
        According to the language reference (and ast.c) the following nodes
1082
        are are possible:
1083
        Name, Attribute, Subscript
1084

1085
        Note that although augmented assignment of attributes and
1086
        subscripts is disallowed, augmented assignment of names (such
1087
        as 'n += 1') is allowed.
1088
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1089
        """
1090

1091
        node = self.node_contents_visit(node)
1✔
1092

1093
        if isinstance(node.target, ast.Attribute):
1✔
1094
            self.error(
1✔
1095
                node,
1096
                "Augmented assignment of attributes is not allowed.")
1097
            return node
1✔
1098

1099
        elif isinstance(node.target, ast.Subscript):
1✔
1100
            self.error(
1✔
1101
                node,
1102
                "Augmented assignment of object items "
1103
                "and slices is not allowed.")
1104
            return node
1✔
1105

1106
        elif isinstance(node.target, ast.Name):
1✔
1107
            new_node = ast.Assign(
1✔
1108
                targets=[node.target],
1109
                value=ast.Call(
1110
                    func=ast.Name('_inplacevar_', ast.Load()),
1111
                    args=[
1112
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1113
                        ast.Name(node.target.id, ast.Load()),
1114
                        node.value
1115
                    ],
1116
                    keywords=[]))
1117

1118
            copy_locations(new_node, node)
1✔
1119
            return new_node
1✔
1120
        else:  # pragma: no cover
1121
            # Impossible Case - Only Node Types:
1122
            # * Name
1123
            # * Attribute
1124
            # * Subscript
1125
            # defined, those are checked before.
1126
            raise NotImplementedError(
1127
                f"Unknown target type: {type(node.target)}")
1128

1129
    def visit_Raise(self, node: ast.Raise) -> ast.AST:
1✔
1130
        """Allow `raise` statements without restrictions."""
1131
        return self.node_contents_visit(node)
1✔
1132

1133
    def visit_Assert(self, node: ast.Assert) -> ast.AST:
1✔
1134
        """Allow assert statements without restrictions."""
1135
        return self.node_contents_visit(node)
1✔
1136

1137
    def visit_Delete(self, node: ast.Delete) -> ast.AST:
1✔
1138
        """Allow `del` statements without restrictions."""
1139
        return self.node_contents_visit(node)
1✔
1140

1141
    def visit_Pass(self, node: ast.Pass) -> ast.AST:
1✔
1142
        """Allow `pass` statements without restrictions."""
1143
        return self.node_contents_visit(node)
1✔
1144

1145
    # Imports
1146

1147
    def visit_Import(self, node: ast.Import) -> ast.AST:
1✔
1148
        """Allow `import` statements with restrictions.
1149
        See check_import_names."""
1150
        return self.check_import_names(node)
1✔
1151

1152
    def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST:
1✔
1153
        """Allow `import from` statements with restrictions.
1154
        See check_import_names."""
1155
        return self.check_import_names(node)
1✔
1156

1157
    def visit_alias(self, node: ast.alias) -> ast.AST:
1✔
1158
        """Allow `as` statements in import and import from statements."""
1159
        return self.node_contents_visit(node)
1✔
1160

1161
    # Control flow
1162

1163
    def visit_If(self, node: ast.If) -> ast.AST:
1✔
1164
        """Allow `if` statements without restrictions."""
1165
        return self.node_contents_visit(node)
1✔
1166

1167
    def visit_For(self, node: ast.For) -> ast.AST:
1✔
1168
        """Allow `for` statements with some restrictions."""
1169
        return self.guard_iter(node)
1✔
1170

1171
    def visit_While(self, node: ast.While) -> ast.AST:
1✔
1172
        """Allow `while` statements."""
1173
        return self.node_contents_visit(node)
1✔
1174

1175
    def visit_Break(self, node: ast.Break) -> ast.AST:
1✔
1176
        """Allow `break` statements without restrictions."""
1177
        return self.node_contents_visit(node)
1✔
1178

1179
    def visit_Continue(self, node: ast.Continue) -> ast.AST:
1✔
1180
        """Allow `continue` statements without restrictions."""
1181
        return self.node_contents_visit(node)
1✔
1182

1183
    def visit_Try(self, node: ast.Try) -> ast.AST:
1✔
1184
        """Allow `try` without restrictions."""
1185
        return self.node_contents_visit(node)
1✔
1186

1187
    def visit_TryStar(self, node: ast.AST) -> ast.AST:
1✔
1188
        """Disallow `ExceptionGroup` due to a potential sandbox escape.
1189

1190
        TODO: Type Annotation for node when dropping support
1191
              for Python < 3.11 should be ast.TryStar.
1192
        """
1193
        self.not_allowed(node)
1✔
1194

1195
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> ast.AST:
1✔
1196
        """Protect exception handlers."""
1197
        node = self.node_contents_visit(node)
1✔
1198
        self.check_name(node, node.name)
1✔
1199
        return node
1✔
1200

1201
    def visit_With(self, node: ast.With) -> ast.AST:
1✔
1202
        """Protect tuple unpacking on with statements."""
1203
        node = self.node_contents_visit(node)
1✔
1204

1205
        for item in reversed(node.items):
1✔
1206
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1207
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1208
                    node,
1209
                    item.optional_vars)
1210

1211
                item.optional_vars = tmp_target
1✔
1212
                node.body.insert(0, unpack)
1✔
1213

1214
        return node
1✔
1215

1216
    def visit_withitem(self, node: ast.withitem) -> ast.AST:
1✔
1217
        """Allow `with` statements (context managers) without restrictions."""
1218
        return self.node_contents_visit(node)
1✔
1219

1220
    # Function and class definitions
1221

1222
    def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
1✔
1223
        """Allow function definitions (`def`) with some restrictions."""
1224
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1225
        self.check_function_argument_names(node)
1✔
1226

1227
        with self.print_info.new_print_scope():
1✔
1228
            node = self.node_contents_visit(node)
1✔
1229
            self.inject_print_collector(node)
1✔
1230
        return node
1✔
1231

1232
    def visit_Lambda(self, node: ast.Lambda) -> ast.AST:
1✔
1233
        """Allow lambda with some restrictions."""
1234
        self.check_function_argument_names(node)
1✔
1235
        return self.node_contents_visit(node)
1✔
1236

1237
    def visit_arguments(self, node: ast.arguments) -> ast.AST:
1✔
1238
        """
1239

1240
        """
1241
        return self.node_contents_visit(node)
1✔
1242

1243
    def visit_arg(self, node: ast.arg) -> ast.AST:
1✔
1244
        """
1245

1246
        """
1247
        return self.node_contents_visit(node)
1✔
1248

1249
    def visit_Return(self, node: ast.Return) -> ast.AST:
1✔
1250
        """Allow `return` statements without restrictions."""
1251
        return self.node_contents_visit(node)
1✔
1252

1253
    def visit_Yield(self, node: ast.Yield) -> ast.AST:
1✔
1254
        """Allow `yield`statements without restrictions."""
1255
        return self.node_contents_visit(node)
1✔
1256

1257
    def visit_YieldFrom(self, node: ast.YieldFrom) -> ast.AST:
1✔
1258
        """Allow `yield`statements without restrictions."""
1259
        return self.node_contents_visit(node)
1✔
1260

1261
    def visit_Global(self, node: ast.Global) -> ast.AST:
1✔
1262
        """Allow `global` statements without restrictions."""
1263
        return self.node_contents_visit(node)
1✔
1264

1265
    def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.AST:
1✔
1266
        """Deny `nonlocal` statements."""
1267
        self.not_allowed(node)
1✔
1268

1269
    def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST:
1✔
1270
        """Check the name of a class definition."""
1271
        self.check_name(node, node.name)
1✔
1272
        node = self.node_contents_visit(node)
1✔
1273
        if any(keyword.arg == 'metaclass' for keyword in node.keywords):
1✔
1274
            self.error(
1✔
1275
                node, 'The keyword argument "metaclass" is not allowed.')
1276
        CLASS_DEF = textwrap.dedent('''\
1✔
1277
            class {0.name}(metaclass=__metaclass__):
1278
                pass
1279
        '''.format(node))
1280
        new_class_node = ast.parse(CLASS_DEF).body[0]
1✔
1281
        new_class_node.body = node.body
1✔
1282
        new_class_node.bases = node.bases
1✔
1283
        new_class_node.decorator_list = node.decorator_list
1✔
1284
        return new_class_node
1✔
1285

1286
    def visit_Module(self, node: ast.Module) -> ast.AST:
1✔
1287
        """Add the print_collector (only if print is used) at the top."""
1288
        node = self.node_contents_visit(node)
1✔
1289

1290
        # Inject the print collector after 'from __future__ import ....'
1291
        position = 0
1✔
1292
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1293
            if not isinstance(child, ast.ImportFrom):
1✔
1294
                break
1✔
1295

1296
            if not child.module == '__future__':
1✔
1297
                break
1✔
1298

1299
        self.inject_print_collector(node, position)
1✔
1300
        return node
1✔
1301

1302
    # Async und await
1303

1304
    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST:
1✔
1305
        """Deny async functions."""
1306
        self.not_allowed(node)
1✔
1307

1308
    def visit_Await(self, node: ast.Await) -> ast.AST:
1✔
1309
        """Deny async functionality."""
1310
        self.not_allowed(node)
1✔
1311

1312
    def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AST:
1✔
1313
        """Deny async functionality."""
1314
        self.not_allowed(node)
1✔
1315

1316
    def visit_AsyncWith(self, node: ast.AsyncWith) -> ast.AST:
1✔
1317
        """Deny async functionality."""
1318
        self.not_allowed(node)
1✔
1319

1320
    # Assignment expressions (walrus operator ``:=``)
1321
    # New in 3.8
1322
    def visit_NamedExpr(self, node: ast.NamedExpr) -> ast.AST:
1✔
1323
        """Allow assignment expressions under some circumstances."""
1324
        # while the grammar requires ``node.target`` to be a ``Name``
1325
        # the abstract syntax is more permissive and allows an ``expr``.
1326
        # We support only a ``Name``.
1327
        # This is safe as the expression can only add/modify local
1328
        # variables. While this may hide global variables, an
1329
        # (implicitly performed) name check guarantees (as usual)
1330
        # that no essential global variable is hidden.
1331
        node = self.node_contents_visit(node)  # this checks ``node.target``
1✔
1332
        target = node.target
1✔
1333
        if not isinstance(target, ast.Name):
1✔
1334
            self.error(
1✔
1335
                node,
1336
                "Assignment expressions are only allowed for simple targets")
1337
        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