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

zopefoundation / RestrictedPython / 18613662438

18 Oct 2025 09:09AM UTC coverage: 98.778% (+0.006%) from 98.772%
18613662438

Pull #303

github

loechel
Remove License Cassifier, as they are deprecated
Pull Request #303: Type Annotations for RestrictedPython

213 of 231 branches covered (92.21%)

131 of 132 new or added lines in 4 files covered. (99.24%)

3 existing lines in 2 files now uncovered.

2505 of 2536 relevant lines covered (98.78%)

0.99 hits per line

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

95.37
/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_JoinedStr(self, node: ast.JoinedStr) -> ast.AST:
1✔
579
        """Allow joined string without restrictions."""
580
        return self.node_contents_visit(node)
1✔
581

582
    # ast for Variables
583

584
    def visit_Name(self, node: ast.Name) -> ast.Name | None:
1✔
585
        """Prevents access to protected names.
586

587
        Converts use of the name 'printed' to this expression: '_print()'
588
        """
589

590
        node = self.node_contents_visit(node)
1✔
591

592
        if isinstance(node.ctx, ast.Load):
1✔
593
            if node.id == 'printed':
1✔
594
                self.print_info.printed_used = True
1✔
595
                new_node = ast.Call(
1✔
596
                    func=ast.Name("_print", ast.Load()),
597
                    args=[],
598
                    keywords=[])
599

600
                copy_locations(new_node, node)
1✔
601
                return new_node
1✔
602

603
            elif node.id == 'print':
1✔
604
                self.print_info.print_used = True
1✔
605
                new_node = ast.Attribute(
1✔
606
                    value=ast.Name('_print', ast.Load()),
607
                    attr="_call_print",
608
                    ctx=ast.Load())
609

610
                copy_locations(new_node, node)
1✔
611
                return new_node
1✔
612

613
            self.used_names[node.id] = True
1✔
614

615
        self.check_name(node, node.id)
1✔
616
        return node
1✔
617

618
    def visit_Load(self, node: ast.Load) -> ast.Load | None:
1✔
619
        """
620

621
        """
622
        return self.node_contents_visit(node)
1✔
623

624
    def visit_Store(self, node: ast.Store) -> ast.AST:
1✔
625
        """
626

627
        """
628
        return self.node_contents_visit(node)
1✔
629

630
    def visit_Del(self, node: ast.Del) -> ast.Del:
1✔
631
        """
632

633
        """
634
        return self.node_contents_visit(node)
1✔
635

636
    def visit_Starred(self, node: ast.Starred) -> ast.AST:
1✔
637
        """
638

639
        """
640
        return self.node_contents_visit(node)
1✔
641

642
    # Expressions
643

644
    def visit_Expression(self, node: ast.Expression) -> ast.AST:
1✔
645
        """Allow Expression statements without restrictions.
646

647
        They are in the AST when using the `eval` compile mode.
648
        """
649
        return self.node_contents_visit(node)
1✔
650

651
    def visit_Expr(self, node: ast.Expr) -> ast.AST:
1✔
652
        """Allow Expr statements (any expression) without restrictions."""
653
        return self.node_contents_visit(node)
1✔
654

655
    def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST:
1✔
656
        """
657
        UnaryOp (Unary Operations) is the overall element for:
658
        * Not --> which should be allowed
659
        * UAdd --> Positive notation of variables (e.g. +var)
660
        * USub --> Negative notation of variables (e.g. -var)
661
        """
662
        return self.node_contents_visit(node)
1✔
663

664
    def visit_UAdd(self, node: ast.UAdd) -> ast.AST:
1✔
665
        """Allow positive notation of variables. (e.g. +var)"""
666
        return self.node_contents_visit(node)
1✔
667

668
    def visit_USub(self, node: ast.USub) -> ast.AST:
1✔
669
        """Allow negative notation of variables. (e.g. -var)"""
670
        return self.node_contents_visit(node)
1✔
671

672
    def visit_Not(self, node: ast.Not) -> ast.AST:
1✔
673
        """Allow the `not` operator."""
674
        return self.node_contents_visit(node)
1✔
675

676
    def visit_Invert(self, node: ast.Invert) -> ast.AST:
1✔
677
        """Allow `~` expressions."""
678
        return self.node_contents_visit(node)
1✔
679

680
    def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
1✔
681
        """Allow binary operations."""
682
        return self.node_contents_visit(node)
1✔
683

684
    def visit_Add(self, node: ast.Add) -> ast.AST:
1✔
685
        """Allow `+` expressions."""
686
        return self.node_contents_visit(node)
1✔
687

688
    def visit_Sub(self, node: ast.Sub) -> ast.AST:
1✔
689
        """Allow `-` expressions."""
690
        return self.node_contents_visit(node)
1✔
691

692
    def visit_Mult(self, node: ast.Mult) -> ast.AST:
1✔
693
        """Allow `*` expressions."""
694
        return self.node_contents_visit(node)
1✔
695

696
    def visit_Div(self, node: ast.Div) -> ast.AST:
1✔
697
        """Allow `/` expressions."""
698
        return self.node_contents_visit(node)
1✔
699

700
    def visit_FloorDiv(self, node: ast.FloorDiv) -> ast.AST:
1✔
701
        """Allow `//` expressions."""
702
        return self.node_contents_visit(node)
1✔
703

704
    def visit_Mod(self, node: ast.Mod) -> ast.AST:
1✔
705
        """Allow `%` expressions."""
706
        return self.node_contents_visit(node)
1✔
707

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

712
    def visit_LShift(self, node: ast.LShift) -> ast.AST:
1✔
713
        """Allow `<<` expressions."""
714
        return self.node_contents_visit(node)
1✔
715

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

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

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

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

732
    def visit_MatMult(self, node: ast.MatMult) -> ast.AST:
1✔
733
        """Allow multiplication (`@`)."""
734
        return self.node_contents_visit(node)
1✔
735

736
    def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST:
1✔
737
        """Allow bool operator without restrictions."""
738
        return self.node_contents_visit(node)
1✔
739

740
    def visit_And(self, node: ast.And) -> ast.AST:
1✔
741
        """Allow bool operator `and` without restrictions."""
742
        return self.node_contents_visit(node)
1✔
743

744
    def visit_Or(self, node: ast.Or) -> ast.AST:
1✔
745
        """Allow bool operator `or` without restrictions."""
746
        return self.node_contents_visit(node)
1✔
747

748
    def visit_Compare(self, node: ast.Compare) -> ast.AST:
1✔
749
        """Allow comparison expressions without restrictions."""
750
        return self.node_contents_visit(node)
1✔
751

752
    def visit_Eq(self, node: ast.Eq) -> ast.AST:
1✔
753
        """Allow == expressions."""
754
        return self.node_contents_visit(node)
1✔
755

756
    def visit_NotEq(self, node: ast.NotEq) -> ast.AST:
1✔
757
        """Allow != expressions."""
758
        return self.node_contents_visit(node)
1✔
759

760
    def visit_Lt(self, node: ast.Lt) -> ast.AST:
1✔
761
        """Allow < expressions."""
762
        return self.node_contents_visit(node)
1✔
763

764
    def visit_LtE(self, node: ast.LtE) -> ast.AST:
1✔
765
        """Allow <= expressions."""
766
        return self.node_contents_visit(node)
1✔
767

768
    def visit_Gt(self, node: ast.Gt) -> ast.AST:
1✔
769
        """Allow > expressions."""
770
        return self.node_contents_visit(node)
1✔
771

772
    def visit_GtE(self, node: ast.GtE) -> ast.AST:
1✔
773
        """Allow >= expressions."""
774
        return self.node_contents_visit(node)
1✔
775

776
    def visit_Is(self, node: ast.Is) -> ast.AST:
1✔
777
        """Allow `is` expressions."""
778
        return self.node_contents_visit(node)
1✔
779

780
    def visit_IsNot(self, node: ast.IsNot) -> ast.AST:
1✔
781
        """Allow `is not` expressions."""
782
        return self.node_contents_visit(node)
1✔
783

784
    def visit_In(self, node: ast.In) -> ast.AST:
1✔
785
        """Allow `in` expressions."""
786
        return self.node_contents_visit(node)
1✔
787

788
    def visit_NotIn(self, node: ast.NotIn) -> ast.AST:
1✔
789
        """Allow `not in` expressions."""
790
        return self.node_contents_visit(node)
1✔
791

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

795
        Note: The following happens only if '*args' or '**kwargs' is used.
796

797
        Transfroms 'foo(<all the possible ways of args>)' into
798
        _apply_(foo, <all the possible ways for args>)
799

800
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
801
        Python to collapse all the myriad ways to call functions
802
        into one manageable from.
803

804
        From there, '_apply_()' wraps args and kws in guarded accessors,
805
        then calls the function, returning the value.
806
        """
807

808
        if isinstance(node.func, ast.Name):
1✔
809
            if node.func.id == 'exec':
1✔
810
                self.error(node, 'Exec calls are not allowed.')
1✔
811
            elif node.func.id == 'eval':
1✔
812
                self.error(node, 'Eval calls are not allowed.')
1✔
813

814
        needs_wrap = False
1✔
815

816
        for pos_arg in node.args:
1✔
817
            if isinstance(pos_arg, ast.Starred):
1✔
818
                needs_wrap = True
1✔
819

820
        for keyword_arg in node.keywords:
1✔
821
            if keyword_arg.arg is None:
1✔
822
                needs_wrap = True
1✔
823

824
        node = self.node_contents_visit(node)
1✔
825

826
        if not needs_wrap:
1✔
827
            return node
1✔
828

829
        node.args.insert(0, node.func)
1✔
830
        node.func = ast.Name('_apply_', ast.Load())
1✔
831
        copy_locations(node.func, node.args[0])
1✔
832
        return node
1✔
833

834
    def visit_keyword(self, node: ast.keyword) -> ast.AST:
1✔
835
        """
836

837
        """
838
        return self.node_contents_visit(node)
1✔
839

840
    def visit_IfExp(self, node: ast.IfExp) -> ast.AST:
1✔
841
        """Allow `if` expressions without restrictions."""
842
        return self.node_contents_visit(node)
1✔
843

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

847
        'a.b' becomes '_getattr_(a, "b")'
848
        'a.b = c' becomes '_write_(a).b = c'
849
        'del a.b' becomes 'del _write_(a).b'
850

851
        The _write_ function should return a security proxy.
852
        """
853
        if node.attr.startswith('_') and node.attr != '_':
1✔
854
            self.error(
1✔
855
                node,
856
                '"{name}" is an invalid attribute name because it starts '
857
                'with "_".'.format(name=node.attr))
858

859
        if node.attr.endswith('__roles__'):
1✔
860
            self.error(
1✔
861
                node,
862
                '"{name}" is an invalid attribute name because it ends '
863
                'with "__roles__".'.format(name=node.attr))
864

865
        if node.attr in INSPECT_ATTRIBUTES:
1✔
866
            self.error(
1✔
867
                node,
868
                f'"{node.attr}" is a restricted name,'
869
                ' that is forbidden to access in RestrictedPython.',
870
            )
871

872
        if isinstance(node.ctx, ast.Load):
1✔
873
            node = self.node_contents_visit(node)
1✔
874
            new_node = ast.Call(
1✔
875
                func=ast.Name('_getattr_', ast.Load()),
876
                args=[node.value, ast.Constant(node.attr)],
877
                keywords=[])
878

879
            copy_locations(new_node, node)
1✔
880
            return new_node
1✔
881

882
        elif isinstance(node.ctx, (ast.Store, ast.Del)):
1✔
883
            node = self.node_contents_visit(node)
1✔
884
            new_value = ast.Call(
1✔
885
                func=ast.Name('_write_', ast.Load()),
886
                args=[node.value],
887
                keywords=[])
888

889
            copy_locations(new_value, node.value)
1✔
890
            node.value = new_value
1✔
891
            return node
1✔
892

893
        else:  # pragma: no cover
894
            # Impossible Case only ctx Load, Store and Del are defined in ast.
895
            raise NotImplementedError(
896
                f"Unknown ctx type: {type(node.ctx)}")
897

898
    # Subscripting
899

900
    def visit_Subscript(self, node: ast.Subscript) -> ast.AST:
1✔
901
        """Transforms all kinds of subscripts.
902

903
        'foo[bar]' becomes '_getitem_(foo, bar)'
904
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
905
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
906
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
907
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
908
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
909
        'foo[a] = c' becomes '_write_(foo)[a] = c'
910
        'del foo[a]' becomes 'del _write_(foo)[a]'
911

912
        The _write_ function should return a security proxy.
913
        """
914
        node = self.node_contents_visit(node)
1✔
915

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

921
        if isinstance(node.ctx, ast.Load):
1✔
922
            new_node = ast.Call(
1✔
923
                func=ast.Name('_getitem_', ast.Load()),
924
                args=[node.value, self.transform_slice(node.slice)],
925
                keywords=[])
926

927
            copy_locations(new_node, node)
1✔
928
            return new_node
1✔
929

930
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
931
            new_value = ast.Call(
1✔
932
                func=ast.Name('_write_', ast.Load()),
933
                args=[node.value],
934
                keywords=[])
935

936
            copy_locations(new_value, node)
1✔
937
            node.value = new_value
1✔
938
            return node
1✔
939

940
        else:  # pragma: no cover
941
            # Impossible Case only ctx Load, Store and Del are defined in ast.
942
            raise NotImplementedError(
943
                f"Unknown ctx type: {type(node.ctx)}")
944

945
    def visit_Index(self, node: ast.Index) -> ast.AST:
1✔
946
        """
947

948
        """
UNCOV
949
        return self.node_contents_visit(node)
×
950

951
    def visit_Slice(self, node: ast.Slice) -> ast.AST:
1✔
952
        """
953

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

957
    def visit_ExtSlice(self, node: ast.ExtSlice) -> ast.AST:
1✔
958
        """
959

960
        """
UNCOV
961
        return self.node_contents_visit(node)
×
962

963
    # Comprehensions
964

965
    def visit_ListComp(self, node: ast.ListComp) -> ast.AST:
1✔
966
        """
967

968
        """
969
        return self.node_contents_visit(node)
1✔
970

971
    def visit_SetComp(self, node: ast.SetComp) -> ast.AST:
1✔
972
        """
973

974
        """
975
        return self.node_contents_visit(node)
1✔
976

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

980
        """
981
        return self.node_contents_visit(node)
1✔
982

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

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

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

992
        """
993
        return self.guard_iter(node)
1✔
994

995
    # Statements
996

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

1000
        """
1001

1002
        node = self.node_contents_visit(node)
1✔
1003

1004
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
1005
            return node
1✔
1006

1007
        # Handle sequence unpacking.
1008
        # For briefness this example omits cleanup of the temporary variables.
1009
        # Check 'transform_tuple_assign' how its done.
1010
        #
1011
        # - Single target (with nested support)
1012
        # (a, (b, (c, d))) = <exp>
1013
        # is converted to
1014
        # (a, t1) = _getiter_(<exp>)
1015
        # (b, t2) = _getiter_(t1)
1016
        # (c, d) = _getiter_(t2)
1017
        #
1018
        # - Multi targets
1019
        # (a, b) = (c, d) = <exp>
1020
        # is converted to
1021
        # (c, d) = _getiter_(<exp>)
1022
        # (a, b) = _getiter_(<exp>)
1023
        # Why is this valid ? The original bytecode for this multi targets
1024
        # behaves the same way.
1025

1026
        # ast.NodeTransformer works with list results.
1027
        # He injects it at the right place of the node's parent statements.
1028
        new_nodes = []
1✔
1029

1030
        # python fills the right most target first.
1031
        for target in reversed(node.targets):
1✔
1032
            if isinstance(target, ast.Tuple):
1✔
1033
                wrapper = ast.Assign(
1✔
1034
                    targets=[target],
1035
                    value=self.protect_unpack_sequence(target, node.value))
1036
                new_nodes.append(wrapper)
1✔
1037
            else:
1038
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1039
                new_nodes.append(new_node)
1✔
1040

1041
        for new_node in new_nodes:
1✔
1042
            copy_locations(new_node, node)
1✔
1043

1044
        return new_nodes
1✔
1045

1046
    def visit_AugAssign(self, node: ast.AugAssign) -> ast.AST:
1✔
1047
        """Forbid certain kinds of AugAssign
1048

1049
        According to the language reference (and ast.c) the following nodes
1050
        are are possible:
1051
        Name, Attribute, Subscript
1052

1053
        Note that although augmented assignment of attributes and
1054
        subscripts is disallowed, augmented assignment of names (such
1055
        as 'n += 1') is allowed.
1056
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1057
        """
1058

1059
        node = self.node_contents_visit(node)
1✔
1060

1061
        if isinstance(node.target, ast.Attribute):
1✔
1062
            self.error(
1✔
1063
                node,
1064
                "Augmented assignment of attributes is not allowed.")
1065
            return node
1✔
1066

1067
        elif isinstance(node.target, ast.Subscript):
1✔
1068
            self.error(
1✔
1069
                node,
1070
                "Augmented assignment of object items "
1071
                "and slices is not allowed.")
1072
            return node
1✔
1073

1074
        elif isinstance(node.target, ast.Name):
1✔
1075
            new_node = ast.Assign(
1✔
1076
                targets=[node.target],
1077
                value=ast.Call(
1078
                    func=ast.Name('_inplacevar_', ast.Load()),
1079
                    args=[
1080
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1081
                        ast.Name(node.target.id, ast.Load()),
1082
                        node.value
1083
                    ],
1084
                    keywords=[]))
1085

1086
            copy_locations(new_node, node)
1✔
1087
            return new_node
1✔
1088
        else:  # pragma: no cover
1089
            # Impossible Case - Only Node Types:
1090
            # * Name
1091
            # * Attribute
1092
            # * Subscript
1093
            # defined, those are checked before.
1094
            raise NotImplementedError(
1095
                f"Unknown target type: {type(node.target)}")
1096

1097
    def visit_Raise(self, node: ast.Raise) -> ast.AST:
1✔
1098
        """Allow `raise` statements without restrictions."""
1099
        return self.node_contents_visit(node)
1✔
1100

1101
    def visit_Assert(self, node: ast.Assert) -> ast.AST:
1✔
1102
        """Allow assert statements without restrictions."""
1103
        return self.node_contents_visit(node)
1✔
1104

1105
    def visit_Delete(self, node: ast.Delete) -> ast.AST:
1✔
1106
        """Allow `del` statements without restrictions."""
1107
        return self.node_contents_visit(node)
1✔
1108

1109
    def visit_Pass(self, node: ast.Pass) -> ast.AST:
1✔
1110
        """Allow `pass` statements without restrictions."""
1111
        return self.node_contents_visit(node)
1✔
1112

1113
    # Imports
1114

1115
    def visit_Import(self, node: ast.Import) -> ast.AST:
1✔
1116
        """Allow `import` statements with restrictions.
1117
        See check_import_names."""
1118
        return self.check_import_names(node)
1✔
1119

1120
    def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST:
1✔
1121
        """Allow `import from` statements with restrictions.
1122
        See check_import_names."""
1123
        return self.check_import_names(node)
1✔
1124

1125
    def visit_alias(self, node: ast.alias) -> ast.AST:
1✔
1126
        """Allow `as` statements in import and import from statements."""
1127
        return self.node_contents_visit(node)
1✔
1128

1129
    # Control flow
1130

1131
    def visit_If(self, node: ast.If) -> ast.AST:
1✔
1132
        """Allow `if` statements without restrictions."""
1133
        return self.node_contents_visit(node)
1✔
1134

1135
    def visit_For(self, node: ast.For) -> ast.AST:
1✔
1136
        """Allow `for` statements with some restrictions."""
1137
        return self.guard_iter(node)
1✔
1138

1139
    def visit_While(self, node: ast.While) -> ast.AST:
1✔
1140
        """Allow `while` statements."""
1141
        return self.node_contents_visit(node)
1✔
1142

1143
    def visit_Break(self, node: ast.Break) -> ast.AST:
1✔
1144
        """Allow `break` statements without restrictions."""
1145
        return self.node_contents_visit(node)
1✔
1146

1147
    def visit_Continue(self, node: ast.Continue) -> ast.AST:
1✔
1148
        """Allow `continue` statements without restrictions."""
1149
        return self.node_contents_visit(node)
1✔
1150

1151
    def visit_Try(self, node: ast.Try) -> ast.AST:
1✔
1152
        """Allow `try` without restrictions."""
1153
        return self.node_contents_visit(node)
1✔
1154

1155
    def visit_TryStar(self, node: ast.AST) -> ast.AST:
1✔
1156
        """Disallow `ExceptionGroup` due to a potential sandbox escape."""
1157
        self.not_allowed(node)
1✔
1158

1159
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> ast.AST:
1✔
1160
        """Protect exception handlers."""
1161
        node = self.node_contents_visit(node)
1✔
1162
        self.check_name(node, node.name)
1✔
1163
        return node
1✔
1164

1165
    def visit_With(self, node: ast.With) -> ast.AST:
1✔
1166
        """Protect tuple unpacking on with statements."""
1167
        node = self.node_contents_visit(node)
1✔
1168

1169
        for item in reversed(node.items):
1✔
1170
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1171
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1172
                    node,
1173
                    item.optional_vars)
1174

1175
                item.optional_vars = tmp_target
1✔
1176
                node.body.insert(0, unpack)
1✔
1177

1178
        return node
1✔
1179

1180
    def visit_withitem(self, node: ast.withitem) -> ast.AST:
1✔
1181
        """Allow `with` statements (context managers) without restrictions."""
1182
        return self.node_contents_visit(node)
1✔
1183

1184
    # Function and class definitions
1185

1186
    def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
1✔
1187
        """Allow function definitions (`def`) with some restrictions."""
1188
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1189
        self.check_function_argument_names(node)
1✔
1190

1191
        with self.print_info.new_print_scope():
1✔
1192
            node = self.node_contents_visit(node)
1✔
1193
            self.inject_print_collector(node)
1✔
1194
        return node
1✔
1195

1196
    def visit_Lambda(self, node: ast.Lambda) -> ast.AST:
1✔
1197
        """Allow lambda with some restrictions."""
1198
        self.check_function_argument_names(node)
1✔
1199
        return self.node_contents_visit(node)
1✔
1200

1201
    def visit_arguments(self, node: ast.arguments) -> ast.AST:
1✔
1202
        """
1203

1204
        """
1205
        return self.node_contents_visit(node)
1✔
1206

1207
    def visit_arg(self, node: ast.arg) -> ast.AST:
1✔
1208
        """
1209

1210
        """
1211
        return self.node_contents_visit(node)
1✔
1212

1213
    def visit_Return(self, node: ast.Return) -> ast.AST:
1✔
1214
        """Allow `return` statements without restrictions."""
1215
        return self.node_contents_visit(node)
1✔
1216

1217
    def visit_Yield(self, node: ast.Yield) -> ast.AST:
1✔
1218
        """Allow `yield`statements without restrictions."""
1219
        return self.node_contents_visit(node)
1✔
1220

1221
    def visit_YieldFrom(self, node: ast.YieldFrom) -> ast.AST:
1✔
1222
        """Allow `yield`statements without restrictions."""
1223
        return self.node_contents_visit(node)
1✔
1224

1225
    def visit_Global(self, node: ast.Global) -> ast.AST:
1✔
1226
        """Allow `global` statements without restrictions."""
1227
        return self.node_contents_visit(node)
1✔
1228

1229
    def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.AST:
1✔
1230
        """Deny `nonlocal` statements."""
1231
        self.not_allowed(node)
1✔
1232

1233
    def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST:
1✔
1234
        """Check the name of a class definition."""
1235
        self.check_name(node, node.name)
1✔
1236
        node = self.node_contents_visit(node)
1✔
1237
        if any(keyword.arg == 'metaclass' for keyword in node.keywords):
1✔
1238
            self.error(
1✔
1239
                node, 'The keyword argument "metaclass" is not allowed.')
1240
        CLASS_DEF = textwrap.dedent('''\
1✔
1241
            class {0.name}(metaclass=__metaclass__):
1242
                pass
1243
        '''.format(node))
1244
        new_class_node = ast.parse(CLASS_DEF).body[0]
1✔
1245
        new_class_node.body = node.body
1✔
1246
        new_class_node.bases = node.bases
1✔
1247
        new_class_node.decorator_list = node.decorator_list
1✔
1248
        return new_class_node
1✔
1249

1250
    def visit_Module(self, node: ast.Module) -> ast.AST:
1✔
1251
        """Add the print_collector (only if print is used) at the top."""
1252
        node = self.node_contents_visit(node)
1✔
1253

1254
        # Inject the print collector after 'from __future__ import ....'
1255
        position = 0
1✔
1256
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1257
            if not isinstance(child, ast.ImportFrom):
1✔
1258
                break
1✔
1259

1260
            if not child.module == '__future__':
1✔
1261
                break
1✔
1262

1263
        self.inject_print_collector(node, position)
1✔
1264
        return node
1✔
1265

1266
    # Async und await
1267

1268
    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST:
1✔
1269
        """Deny async functions."""
1270
        self.not_allowed(node)
1✔
1271

1272
    def visit_Await(self, node: ast.Await) -> ast.AST:
1✔
1273
        """Deny async functionality."""
1274
        self.not_allowed(node)
1✔
1275

1276
    def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AST:
1✔
1277
        """Deny async functionality."""
1278
        self.not_allowed(node)
1✔
1279

1280
    def visit_AsyncWith(self, node: ast.AsyncWith) -> ast.AST:
1✔
1281
        """Deny async functionality."""
1282
        self.not_allowed(node)
1✔
1283

1284
    # Assignment expressions (walrus operator ``:=``)
1285
    # New in 3.8
1286
    def visit_NamedExpr(self, node: ast.NamedExpr) -> ast.AST:
1✔
1287
        """Allow assignment expressions under some circumstances."""
1288
        # while the grammar requires ``node.target`` to be a ``Name``
1289
        # the abstract syntax is more permissive and allows an ``expr``.
1290
        # We support only a ``Name``.
1291
        # This is safe as the expression can only add/modify local
1292
        # variables. While this may hide global variables, an
1293
        # (implicitly performed) name check guarantees (as usual)
1294
        # that no essential global variable is hidden.
1295
        node = self.node_contents_visit(node)  # this checks ``node.target``
1✔
1296
        target = node.target
1✔
1297
        if not isinstance(target, ast.Name):
1✔
1298
            self.error(
1✔
1299
                node,
1300
                "Assignment expressions are only allowed for simple targets")
1301
        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

© 2025 Coveralls, Inc