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

zopefoundation / RestrictedPython / 18616410521

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

Pull #303

github

web-flow
Apply pre-commit code formatting
Pull Request #303: Type Annotations for RestrictedPython

213 of 231 branches covered (92.21%)

133 of 137 new or added lines in 4 files covered. (97.08%)

5 existing lines in 2 files 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(
×
590
            node,
591
            'TemplateStr statements are not yet allowed, please use f-strings or a real template engine instead.')
NEW
UNCOV
592
        self.not_allowed(node)
×
593
        # return self.node_contents_visit(node)
594

595
    def visit_Interpolation(self, node: ast.AST) -> ast.AST:
1✔
596
        """Interpolations are not allowed by default.
597
        As Interpolations are part of Template Strings, they will not be reached in
598
        context of RestrictedPython as Template Strings are not allowed.
599

600
        TODO: Deeper review of security implications of interpolated strings.
601
        TODO: Change Type Annotation to ast.Interpolation when
602
              Support for Python 3.13 is dropped.
603
        """
NEW
UNCOV
604
        self.not_allowed(node)
×
605
        # return self.node_contents_visit(node)
606

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

611
    # ast for Variables
612

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

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

619
        node = self.node_contents_visit(node)
1✔
620

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

629
                copy_locations(new_node, node)
1✔
630
                return new_node
1✔
631

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

639
                copy_locations(new_node, node)
1✔
640
                return new_node
1✔
641

642
            self.used_names[node.id] = True
1✔
643

644
        self.check_name(node, node.id)
1✔
645
        return node
1✔
646

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

650
        """
651
        return self.node_contents_visit(node)
1✔
652

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

656
        """
657
        return self.node_contents_visit(node)
1✔
658

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

662
        """
663
        return self.node_contents_visit(node)
1✔
664

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

668
        """
669
        return self.node_contents_visit(node)
1✔
670

671
    # Expressions
672

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

824
        Note: The following happens only if '*args' or '**kwargs' is used.
825

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

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

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

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

843
        needs_wrap = False
1✔
844

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

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

853
        node = self.node_contents_visit(node)
1✔
854

855
        if not needs_wrap:
1✔
856
            return node
1✔
857

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

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

866
        """
867
        return self.node_contents_visit(node)
1✔
868

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

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

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

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

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

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

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

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

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

918
            copy_locations(new_value, node.value)
1✔
919
            node.value = new_value
1✔
920
            return node
1✔
921

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

927
    # Subscripting
928

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

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

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

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

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

956
            copy_locations(new_node, node)
1✔
957
            return new_node
1✔
958

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

965
            copy_locations(new_value, node)
1✔
966
            node.value = new_value
1✔
967
            return node
1✔
968

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

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

977
        """
UNCOV
978
        return self.node_contents_visit(node)
×
979

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

983
        """
984
        return self.node_contents_visit(node)
1✔
985

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

989
        """
UNCOV
990
        return self.node_contents_visit(node)
×
991

992
    # Comprehensions
993

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

997
        """
998
        return self.node_contents_visit(node)
1✔
999

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

1003
        """
1004
        return self.node_contents_visit(node)
1✔
1005

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

1009
        """
1010
        return self.node_contents_visit(node)
1✔
1011

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

1015
        """
1016
        return self.node_contents_visit(node)
1✔
1017

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

1021
        """
1022
        return self.guard_iter(node)
1✔
1023

1024
    # Statements
1025

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

1029
        """
1030

1031
        node = self.node_contents_visit(node)
1✔
1032

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

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

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

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

1070
        for new_node in new_nodes:
1✔
1071
            copy_locations(new_node, node)
1✔
1072

1073
        return new_nodes
1✔
1074

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

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

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

1088
        node = self.node_contents_visit(node)
1✔
1089

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

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

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

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

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

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

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

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

1142
    # Imports
1143

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

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

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

1158
    # Control flow
1159

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

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

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

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

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

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

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

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

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

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

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

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

1211
        return node
1✔
1212

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

1217
    # Function and class definitions
1218

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

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

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

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

1237
        """
1238
        return self.node_contents_visit(node)
1✔
1239

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

1243
        """
1244
        return self.node_contents_visit(node)
1✔
1245

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

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

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

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

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

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

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

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

1293
            if not child.module == '__future__':
1✔
1294
                break
1✔
1295

1296
        self.inject_print_collector(node, position)
1✔
1297
        return node
1✔
1298

1299
    # Async und await
1300

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

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

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

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

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