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

zopefoundation / RestrictedPython / 18616406728

18 Oct 2025 01:41PM UTC coverage: 98.662% (-0.1%) from 98.772%
18616406728

Pull #303

github

loechel
Disable t-strings
Pull Request #303: Type Annotations for RestrictedPython

213 of 231 branches covered (92.21%)

108 of 112 new or added lines in 4 files covered. (96.43%)

2 existing lines in 1 file now uncovered.

2507 of 2541 relevant lines covered (98.66%)

0.99 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 not allowed by default. 
580
        Even so, that template strings can be useful in context of Template Engines
581
        A Template String itself is not executed itself, but it contain expressions 
582
        and need additional template rendering logic applied to it to be useful.
583
        Those rendering logic would be affected by RestrictedPython as well.        
584
        
585
        TODO: Deeper review of security implications of template strings.
586
        TODO: Change Type Annotation to ast.TemplateStr when
587
              Support for Python 3.13 is dropped.
588
        """
NEW
589
        self.warn(node, 'TemplateStr statements are not yet allowed, please use f-strings or a real template engine instead.')
×
NEW
590
        self.not_allowed(node)
×
591
        # return self.node_contents_visit(node)
592

593
    def visit_Interpolation(self, node: ast.AST) -> ast.AST:
1✔
594
        """Interpolations are not allowed by default.
595
        As Interpolations are part of Template Strings, they will not be reached in 
596
        context of RestrictedPython as Template Strings are not allowed.
597
        
598
        TODO: Deeper review of security implications of interpolated strings.
599
        TODO: Change Type Annotation to ast.Interpolation when
600
              Support for Python 3.13 is dropped.
601
        """
NEW
602
        self.not_allowed(node)
×
603
        # return self.node_contents_visit(node)
604

605
    def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.AST:
1✔
606
        """Allow joined string without restrictions."""
607
        return self.node_contents_visit(node)
1✔
608

609
    # ast for Variables
610

611
    def visit_Name(self, node: ast.Name) -> ast.Name | None:
1✔
612
        """Prevents access to protected names.
613

614
        Converts use of the name 'printed' to this expression: '_print()'
615
        """
616

617
        node = self.node_contents_visit(node)
1✔
618

619
        if isinstance(node.ctx, ast.Load):
1✔
620
            if node.id == 'printed':
1✔
621
                self.print_info.printed_used = True
1✔
622
                new_node = ast.Call(
1✔
623
                    func=ast.Name("_print", ast.Load()),
624
                    args=[],
625
                    keywords=[])
626

627
                copy_locations(new_node, node)
1✔
628
                return new_node
1✔
629

630
            elif node.id == 'print':
1✔
631
                self.print_info.print_used = True
1✔
632
                new_node = ast.Attribute(
1✔
633
                    value=ast.Name('_print', ast.Load()),
634
                    attr="_call_print",
635
                    ctx=ast.Load())
636

637
                copy_locations(new_node, node)
1✔
638
                return new_node
1✔
639

640
            self.used_names[node.id] = True
1✔
641

642
        self.check_name(node, node.id)
1✔
643
        return node
1✔
644

645
    def visit_Load(self, node: ast.Load) -> ast.Load | None:
1✔
646
        """
647

648
        """
649
        return self.node_contents_visit(node)
1✔
650

651
    def visit_Store(self, node: ast.Store) -> ast.AST:
1✔
652
        """
653

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

657
    def visit_Del(self, node: ast.Del) -> ast.Del:
1✔
658
        """
659

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

663
    def visit_Starred(self, node: ast.Starred) -> ast.AST:
1✔
664
        """
665

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

669
    # Expressions
670

671
    def visit_Expression(self, node: ast.Expression) -> ast.AST:
1✔
672
        """Allow Expression statements without restrictions.
673

674
        They are in the AST when using the `eval` compile mode.
675
        """
676
        return self.node_contents_visit(node)
1✔
677

678
    def visit_Expr(self, node: ast.Expr) -> ast.AST:
1✔
679
        """Allow Expr statements (any expression) without restrictions."""
680
        return self.node_contents_visit(node)
1✔
681

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

691
    def visit_UAdd(self, node: ast.UAdd) -> ast.AST:
1✔
692
        """Allow positive notation of variables. (e.g. +var)"""
693
        return self.node_contents_visit(node)
1✔
694

695
    def visit_USub(self, node: ast.USub) -> ast.AST:
1✔
696
        """Allow negative notation of variables. (e.g. -var)"""
697
        return self.node_contents_visit(node)
1✔
698

699
    def visit_Not(self, node: ast.Not) -> ast.AST:
1✔
700
        """Allow the `not` operator."""
701
        return self.node_contents_visit(node)
1✔
702

703
    def visit_Invert(self, node: ast.Invert) -> ast.AST:
1✔
704
        """Allow `~` expressions."""
705
        return self.node_contents_visit(node)
1✔
706

707
    def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
1✔
708
        """Allow binary operations."""
709
        return self.node_contents_visit(node)
1✔
710

711
    def visit_Add(self, node: ast.Add) -> ast.AST:
1✔
712
        """Allow `+` expressions."""
713
        return self.node_contents_visit(node)
1✔
714

715
    def visit_Sub(self, node: ast.Sub) -> ast.AST:
1✔
716
        """Allow `-` expressions."""
717
        return self.node_contents_visit(node)
1✔
718

719
    def visit_Mult(self, node: ast.Mult) -> ast.AST:
1✔
720
        """Allow `*` expressions."""
721
        return self.node_contents_visit(node)
1✔
722

723
    def visit_Div(self, node: ast.Div) -> ast.AST:
1✔
724
        """Allow `/` expressions."""
725
        return self.node_contents_visit(node)
1✔
726

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

731
    def visit_Mod(self, node: ast.Mod) -> ast.AST:
1✔
732
        """Allow `%` expressions."""
733
        return self.node_contents_visit(node)
1✔
734

735
    def visit_Pow(self, node: ast.Pow) -> ast.AST:
1✔
736
        """Allow `**` expressions."""
737
        return self.node_contents_visit(node)
1✔
738

739
    def visit_LShift(self, node: ast.LShift) -> ast.AST:
1✔
740
        """Allow `<<` expressions."""
741
        return self.node_contents_visit(node)
1✔
742

743
    def visit_RShift(self, node: ast.RShift) -> ast.AST:
1✔
744
        """Allow `>>` expressions."""
745
        return self.node_contents_visit(node)
1✔
746

747
    def visit_BitOr(self, node: ast.BitOr) -> ast.AST:
1✔
748
        """Allow `|` expressions."""
749
        return self.node_contents_visit(node)
1✔
750

751
    def visit_BitXor(self, node: ast.BitXor) -> ast.AST:
1✔
752
        """Allow `^` expressions."""
753
        return self.node_contents_visit(node)
1✔
754

755
    def visit_BitAnd(self, node: ast.BitAnd) -> ast.AST:
1✔
756
        """Allow `&` expressions."""
757
        return self.node_contents_visit(node)
1✔
758

759
    def visit_MatMult(self, node: ast.MatMult) -> ast.AST:
1✔
760
        """Allow multiplication (`@`)."""
761
        return self.node_contents_visit(node)
1✔
762

763
    def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST:
1✔
764
        """Allow bool operator without restrictions."""
765
        return self.node_contents_visit(node)
1✔
766

767
    def visit_And(self, node: ast.And) -> ast.AST:
1✔
768
        """Allow bool operator `and` without restrictions."""
769
        return self.node_contents_visit(node)
1✔
770

771
    def visit_Or(self, node: ast.Or) -> ast.AST:
1✔
772
        """Allow bool operator `or` without restrictions."""
773
        return self.node_contents_visit(node)
1✔
774

775
    def visit_Compare(self, node: ast.Compare) -> ast.AST:
1✔
776
        """Allow comparison expressions without restrictions."""
777
        return self.node_contents_visit(node)
1✔
778

779
    def visit_Eq(self, node: ast.Eq) -> ast.AST:
1✔
780
        """Allow == expressions."""
781
        return self.node_contents_visit(node)
1✔
782

783
    def visit_NotEq(self, node: ast.NotEq) -> ast.AST:
1✔
784
        """Allow != expressions."""
785
        return self.node_contents_visit(node)
1✔
786

787
    def visit_Lt(self, node: ast.Lt) -> ast.AST:
1✔
788
        """Allow < expressions."""
789
        return self.node_contents_visit(node)
1✔
790

791
    def visit_LtE(self, node: ast.LtE) -> ast.AST:
1✔
792
        """Allow <= expressions."""
793
        return self.node_contents_visit(node)
1✔
794

795
    def visit_Gt(self, node: ast.Gt) -> ast.AST:
1✔
796
        """Allow > expressions."""
797
        return self.node_contents_visit(node)
1✔
798

799
    def visit_GtE(self, node: ast.GtE) -> ast.AST:
1✔
800
        """Allow >= expressions."""
801
        return self.node_contents_visit(node)
1✔
802

803
    def visit_Is(self, node: ast.Is) -> ast.AST:
1✔
804
        """Allow `is` expressions."""
805
        return self.node_contents_visit(node)
1✔
806

807
    def visit_IsNot(self, node: ast.IsNot) -> ast.AST:
1✔
808
        """Allow `is not` expressions."""
809
        return self.node_contents_visit(node)
1✔
810

811
    def visit_In(self, node: ast.In) -> ast.AST:
1✔
812
        """Allow `in` expressions."""
813
        return self.node_contents_visit(node)
1✔
814

815
    def visit_NotIn(self, node: ast.NotIn) -> ast.AST:
1✔
816
        """Allow `not in` expressions."""
817
        return self.node_contents_visit(node)
1✔
818

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

822
        Note: The following happens only if '*args' or '**kwargs' is used.
823

824
        Transfroms 'foo(<all the possible ways of args>)' into
825
        _apply_(foo, <all the possible ways for args>)
826

827
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
828
        Python to collapse all the myriad ways to call functions
829
        into one manageable from.
830

831
        From there, '_apply_()' wraps args and kws in guarded accessors,
832
        then calls the function, returning the value.
833
        """
834

835
        if isinstance(node.func, ast.Name):
1✔
836
            if node.func.id == 'exec':
1✔
837
                self.error(node, 'Exec calls are not allowed.')
1✔
838
            elif node.func.id == 'eval':
1✔
839
                self.error(node, 'Eval calls are not allowed.')
1✔
840

841
        needs_wrap = False
1✔
842

843
        for pos_arg in node.args:
1✔
844
            if isinstance(pos_arg, ast.Starred):
1✔
845
                needs_wrap = True
1✔
846

847
        for keyword_arg in node.keywords:
1✔
848
            if keyword_arg.arg is None:
1✔
849
                needs_wrap = True
1✔
850

851
        node = self.node_contents_visit(node)
1✔
852

853
        if not needs_wrap:
1✔
854
            return node
1✔
855

856
        node.args.insert(0, node.func)
1✔
857
        node.func = ast.Name('_apply_', ast.Load())
1✔
858
        copy_locations(node.func, node.args[0])
1✔
859
        return node
1✔
860

861
    def visit_keyword(self, node: ast.keyword) -> ast.AST:
1✔
862
        """
863

864
        """
865
        return self.node_contents_visit(node)
1✔
866

867
    def visit_IfExp(self, node: ast.IfExp) -> ast.AST:
1✔
868
        """Allow `if` expressions without restrictions."""
869
        return self.node_contents_visit(node)
1✔
870

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

874
        'a.b' becomes '_getattr_(a, "b")'
875
        'a.b = c' becomes '_write_(a).b = c'
876
        'del a.b' becomes 'del _write_(a).b'
877

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

886
        if node.attr.endswith('__roles__'):
1✔
887
            self.error(
1✔
888
                node,
889
                '"{name}" is an invalid attribute name because it ends '
890
                'with "__roles__".'.format(name=node.attr))
891

892
        if node.attr in INSPECT_ATTRIBUTES:
1✔
893
            self.error(
1✔
894
                node,
895
                f'"{node.attr}" is a restricted name,'
896
                ' that is forbidden to access in RestrictedPython.',
897
            )
898

899
        if isinstance(node.ctx, ast.Load):
1✔
900
            node = self.node_contents_visit(node)
1✔
901
            new_node = ast.Call(
1✔
902
                func=ast.Name('_getattr_', ast.Load()),
903
                args=[node.value, ast.Constant(node.attr)],
904
                keywords=[])
905

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

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

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

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

925
    # Subscripting
926

927
    def visit_Subscript(self, node: ast.Subscript) -> ast.AST:
1✔
928
        """Transforms all kinds of subscripts.
929

930
        'foo[bar]' becomes '_getitem_(foo, bar)'
931
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
932
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
933
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
934
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
935
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
936
        'foo[a] = c' becomes '_write_(foo)[a] = c'
937
        'del foo[a]' becomes 'del _write_(foo)[a]'
938

939
        The _write_ function should return a security proxy.
940
        """
941
        node = self.node_contents_visit(node)
1✔
942

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

948
        if isinstance(node.ctx, ast.Load):
1✔
949
            new_node = ast.Call(
1✔
950
                func=ast.Name('_getitem_', ast.Load()),
951
                args=[node.value, self.transform_slice(node.slice)],
952
                keywords=[])
953

954
            copy_locations(new_node, node)
1✔
955
            return new_node
1✔
956

957
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
958
            new_value = ast.Call(
1✔
959
                func=ast.Name('_write_', ast.Load()),
960
                args=[node.value],
961
                keywords=[])
962

963
            copy_locations(new_value, node)
1✔
964
            node.value = new_value
1✔
965
            return node
1✔
966

967
        else:  # pragma: no cover
968
            # Impossible Case only ctx Load, Store and Del are defined in ast.
969
            raise NotImplementedError(
970
                f"Unknown ctx type: {type(node.ctx)}")
971

972
    def visit_Index(self, node: ast.Index) -> ast.AST:
1✔
973
        """
974

975
        """
UNCOV
976
        return self.node_contents_visit(node)
×
977

978
    def visit_Slice(self, node: ast.Slice) -> ast.AST:
1✔
979
        """
980

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

984
    def visit_ExtSlice(self, node: ast.ExtSlice) -> ast.AST:
1✔
985
        """
986

987
        """
UNCOV
988
        return self.node_contents_visit(node)
×
989

990
    # Comprehensions
991

992
    def visit_ListComp(self, node: ast.ListComp) -> ast.AST:
1✔
993
        """
994

995
        """
996
        return self.node_contents_visit(node)
1✔
997

998
    def visit_SetComp(self, node: ast.SetComp) -> ast.AST:
1✔
999
        """
1000

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

1004
    def visit_GeneratorExp(self, node: ast.GeneratorExp) -> ast.AST:
1✔
1005
        """
1006

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

1010
    def visit_DictComp(self, node: ast.DictComp) -> ast.AST:
1✔
1011
        """
1012

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

1016
    def visit_comprehension(self, node: ast.comprehension) -> ast.AST:
1✔
1017
        """
1018

1019
        """
1020
        return self.guard_iter(node)
1✔
1021

1022
    # Statements
1023

1024
    def visit_Assign(self, node: ast.Assign) -> ast.AST:
1✔
1025
        """
1026

1027
        """
1028

1029
        node = self.node_contents_visit(node)
1✔
1030

1031
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
1032
            return node
1✔
1033

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

1053
        # ast.NodeTransformer works with list results.
1054
        # He injects it at the right place of the node's parent statements.
1055
        new_nodes = []
1✔
1056

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

1068
        for new_node in new_nodes:
1✔
1069
            copy_locations(new_node, node)
1✔
1070

1071
        return new_nodes
1✔
1072

1073
    def visit_AugAssign(self, node: ast.AugAssign) -> ast.AST:
1✔
1074
        """Forbid certain kinds of AugAssign
1075

1076
        According to the language reference (and ast.c) the following nodes
1077
        are are possible:
1078
        Name, Attribute, Subscript
1079

1080
        Note that although augmented assignment of attributes and
1081
        subscripts is disallowed, augmented assignment of names (such
1082
        as 'n += 1') is allowed.
1083
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1084
        """
1085

1086
        node = self.node_contents_visit(node)
1✔
1087

1088
        if isinstance(node.target, ast.Attribute):
1✔
1089
            self.error(
1✔
1090
                node,
1091
                "Augmented assignment of attributes is not allowed.")
1092
            return node
1✔
1093

1094
        elif isinstance(node.target, ast.Subscript):
1✔
1095
            self.error(
1✔
1096
                node,
1097
                "Augmented assignment of object items "
1098
                "and slices is not allowed.")
1099
            return node
1✔
1100

1101
        elif isinstance(node.target, ast.Name):
1✔
1102
            new_node = ast.Assign(
1✔
1103
                targets=[node.target],
1104
                value=ast.Call(
1105
                    func=ast.Name('_inplacevar_', ast.Load()),
1106
                    args=[
1107
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1108
                        ast.Name(node.target.id, ast.Load()),
1109
                        node.value
1110
                    ],
1111
                    keywords=[]))
1112

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

1124
    def visit_Raise(self, node: ast.Raise) -> ast.AST:
1✔
1125
        """Allow `raise` statements without restrictions."""
1126
        return self.node_contents_visit(node)
1✔
1127

1128
    def visit_Assert(self, node: ast.Assert) -> ast.AST:
1✔
1129
        """Allow assert statements without restrictions."""
1130
        return self.node_contents_visit(node)
1✔
1131

1132
    def visit_Delete(self, node: ast.Delete) -> ast.AST:
1✔
1133
        """Allow `del` statements without restrictions."""
1134
        return self.node_contents_visit(node)
1✔
1135

1136
    def visit_Pass(self, node: ast.Pass) -> ast.AST:
1✔
1137
        """Allow `pass` statements without restrictions."""
1138
        return self.node_contents_visit(node)
1✔
1139

1140
    # Imports
1141

1142
    def visit_Import(self, node: ast.Import) -> ast.AST:
1✔
1143
        """Allow `import` statements with restrictions.
1144
        See check_import_names."""
1145
        return self.check_import_names(node)
1✔
1146

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

1152
    def visit_alias(self, node: ast.alias) -> ast.AST:
1✔
1153
        """Allow `as` statements in import and import from statements."""
1154
        return self.node_contents_visit(node)
1✔
1155

1156
    # Control flow
1157

1158
    def visit_If(self, node: ast.If) -> ast.AST:
1✔
1159
        """Allow `if` statements without restrictions."""
1160
        return self.node_contents_visit(node)
1✔
1161

1162
    def visit_For(self, node: ast.For) -> ast.AST:
1✔
1163
        """Allow `for` statements with some restrictions."""
1164
        return self.guard_iter(node)
1✔
1165

1166
    def visit_While(self, node: ast.While) -> ast.AST:
1✔
1167
        """Allow `while` statements."""
1168
        return self.node_contents_visit(node)
1✔
1169

1170
    def visit_Break(self, node: ast.Break) -> ast.AST:
1✔
1171
        """Allow `break` statements without restrictions."""
1172
        return self.node_contents_visit(node)
1✔
1173

1174
    def visit_Continue(self, node: ast.Continue) -> ast.AST:
1✔
1175
        """Allow `continue` statements without restrictions."""
1176
        return self.node_contents_visit(node)
1✔
1177

1178
    def visit_Try(self, node: ast.Try) -> ast.AST:
1✔
1179
        """Allow `try` without restrictions."""
1180
        return self.node_contents_visit(node)
1✔
1181

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

1185
        TODO: Type Annotation for node when dropping support
1186
              for Python < 3.11 should be ast.TryStar.
1187
        """
1188
        self.not_allowed(node)
1✔
1189

1190
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> ast.AST:
1✔
1191
        """Protect exception handlers."""
1192
        node = self.node_contents_visit(node)
1✔
1193
        self.check_name(node, node.name)
1✔
1194
        return node
1✔
1195

1196
    def visit_With(self, node: ast.With) -> ast.AST:
1✔
1197
        """Protect tuple unpacking on with statements."""
1198
        node = self.node_contents_visit(node)
1✔
1199

1200
        for item in reversed(node.items):
1✔
1201
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1202
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1203
                    node,
1204
                    item.optional_vars)
1205

1206
                item.optional_vars = tmp_target
1✔
1207
                node.body.insert(0, unpack)
1✔
1208

1209
        return node
1✔
1210

1211
    def visit_withitem(self, node: ast.withitem) -> ast.AST:
1✔
1212
        """Allow `with` statements (context managers) without restrictions."""
1213
        return self.node_contents_visit(node)
1✔
1214

1215
    # Function and class definitions
1216

1217
    def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
1✔
1218
        """Allow function definitions (`def`) with some restrictions."""
1219
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1220
        self.check_function_argument_names(node)
1✔
1221

1222
        with self.print_info.new_print_scope():
1✔
1223
            node = self.node_contents_visit(node)
1✔
1224
            self.inject_print_collector(node)
1✔
1225
        return node
1✔
1226

1227
    def visit_Lambda(self, node: ast.Lambda) -> ast.AST:
1✔
1228
        """Allow lambda with some restrictions."""
1229
        self.check_function_argument_names(node)
1✔
1230
        return self.node_contents_visit(node)
1✔
1231

1232
    def visit_arguments(self, node: ast.arguments) -> ast.AST:
1✔
1233
        """
1234

1235
        """
1236
        return self.node_contents_visit(node)
1✔
1237

1238
    def visit_arg(self, node: ast.arg) -> ast.AST:
1✔
1239
        """
1240

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

1244
    def visit_Return(self, node: ast.Return) -> ast.AST:
1✔
1245
        """Allow `return` statements without restrictions."""
1246
        return self.node_contents_visit(node)
1✔
1247

1248
    def visit_Yield(self, node: ast.Yield) -> ast.AST:
1✔
1249
        """Allow `yield`statements without restrictions."""
1250
        return self.node_contents_visit(node)
1✔
1251

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

1256
    def visit_Global(self, node: ast.Global) -> ast.AST:
1✔
1257
        """Allow `global` statements without restrictions."""
1258
        return self.node_contents_visit(node)
1✔
1259

1260
    def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.AST:
1✔
1261
        """Deny `nonlocal` statements."""
1262
        self.not_allowed(node)
1✔
1263

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

1281
    def visit_Module(self, node: ast.Module) -> ast.AST:
1✔
1282
        """Add the print_collector (only if print is used) at the top."""
1283
        node = self.node_contents_visit(node)
1✔
1284

1285
        # Inject the print collector after 'from __future__ import ....'
1286
        position = 0
1✔
1287
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1288
            if not isinstance(child, ast.ImportFrom):
1✔
1289
                break
1✔
1290

1291
            if not child.module == '__future__':
1✔
1292
                break
1✔
1293

1294
        self.inject_print_collector(node, position)
1✔
1295
        return node
1✔
1296

1297
    # Async und await
1298

1299
    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST:
1✔
1300
        """Deny async functions."""
1301
        self.not_allowed(node)
1✔
1302

1303
    def visit_Await(self, node: ast.Await) -> ast.AST:
1✔
1304
        """Deny async functionality."""
1305
        self.not_allowed(node)
1✔
1306

1307
    def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AST:
1✔
1308
        """Deny async functionality."""
1309
        self.not_allowed(node)
1✔
1310

1311
    def visit_AsyncWith(self, node: ast.AsyncWith) -> ast.AST:
1✔
1312
        """Deny async functionality."""
1313
        self.not_allowed(node)
1✔
1314

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