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

zopefoundation / RestrictedPython / 26894310350

03 Jun 2026 03:15PM UTC coverage: 98.993% (+0.02%) from 98.972%
26894310350

Pull #317

github

web-flow
Merge 22ced9021 into a2891c0d1
Pull Request #317: Type Annotations for RestrictedPython

212 of 219 branches covered (96.8%)

120 of 120 new or added lines in 8 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

2556 of 2582 relevant lines covered (98.99%)

0.99 hits per line

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

99.57
/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 collections
1✔
23
import contextlib
1✔
24
import sys
1✔
25
import textwrap
1✔
26
import typing
1✔
27

28

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

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

59

60
FORBIDDEN_FUNC_NAMES = frozenset([
1✔
61
    'print',
62
    'printed',
63
    'builtins',
64
    'breakpoint',
65
])
66

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

117
_T_visit_return: typing.TypeAlias = ast.AST | typing.Iterable[ast.AST] | None
1✔
118
_T_pos_ast: typing.TypeAlias = (
1✔
119
    ast.stmt | ast.expr | ast.excepthandler | ast.arg | ast.keyword | ast.alias
120
    | ast.pattern)
121
if sys.version_info >= (3, 12):
1!
122
    _T_pos_ast: typing.TypeAlias = _T_pos_ast | ast.type_param
1✔
123
_T = typing.TypeVar('_T', bound=ast.AST)
1✔
124

125
# When new ast nodes are generated they have no 'lineno', 'end_lineno',
126
# 'col_offset' and 'end_col_offset'. This function copies these fields from the
127
# incoming node:
128

129

130
def copy_locations(new_node: _T_pos_ast, old_node: _T_pos_ast) -> None:
1✔
131
    assert 'lineno' in new_node._attributes
1✔
132
    new_node.lineno = old_node.lineno
1✔
133

134
    assert 'end_lineno' in new_node._attributes
1✔
135
    new_node.end_lineno = old_node.end_lineno
1✔
136

137
    assert 'col_offset' in new_node._attributes
1✔
138
    new_node.col_offset = old_node.col_offset
1✔
139

140
    assert 'end_col_offset' in new_node._attributes
1✔
141
    new_node.end_col_offset = old_node.end_col_offset
1✔
142

143
    ast.fix_missing_locations(new_node)
1✔
144

145

146
class PrintInfo:
1✔
147
    def __init__(self) -> None:
1✔
148
        self.print_used = False
1✔
149
        self.printed_used = False
1✔
150

151
    @contextlib.contextmanager
1✔
152
    def new_print_scope(self) -> collections.abc.Iterator[None]:
1✔
153
        old_print_used = self.print_used
1✔
154
        old_printed_used = self.printed_used
1✔
155

156
        self.print_used = False
1✔
157
        self.printed_used = False
1✔
158

159
        try:
1✔
160
            yield
1✔
161
        finally:
162
            self.print_used = old_print_used
1✔
163
            self.printed_used = old_printed_used
1✔
164

165

166
class RestrictingNodeTransformer(ast.NodeTransformer):
1✔
167
    errors: list[str]
1✔
168
    warnings: list[str]
1✔
169
    used_names: dict[str, bool]
1✔
170

171
    def __init__(self,
1✔
172
                 errors: list[str] | None = None,
173
                 warnings: list[str] | None = None,
174
                 used_names: dict[str, bool] | None = None):
175
        super().__init__()
1✔
176
        self.errors = [] if errors is None else errors
1✔
177
        self.warnings = [] if warnings is None else warnings
1✔
178

179
        # All the variables used by the incoming source.
180
        # Internal names/variables, like the ones from 'gen_tmp_name', don't
181
        # have to be added.
182
        # 'used_names' is for example needed by 'RestrictionCapableEval' to
183
        # know wich names it has to supply when calling the final code.
184
        self.used_names = {} if used_names is None else used_names
1✔
185

186
        # Global counter to construct temporary variable names.
187
        self._tmp_idx = 0
1✔
188

189
        self.print_info = PrintInfo()
1✔
190

191
    def gen_tmp_name(self) -> str:
1✔
192
        # 'check_name' ensures that no variable is prefixed with '_'.
193
        # => Its safe to use '_tmp..' as a temporary variable.
194
        name = '_tmp%i' % self._tmp_idx
1✔
195
        self._tmp_idx += 1
1✔
196
        return name
1✔
197

198
    def error(self, node: ast.AST, info: str) -> None:
1✔
199
        """Record a security error discovered during transformation."""
200
        lineno = getattr(node, 'lineno', None)
1✔
201
        self.errors.append(
1✔
202
            f'Line {lineno}: {info}')
203

204
    def warn(self, node: ast.AST, info: str) -> None:
1✔
205
        """Record a security warning discovered during transformation."""
206
        lineno = getattr(node, 'lineno', None)
1✔
207
        self.warnings.append(
1✔
208
            f'Line {lineno}: {info}')
209

210
    def guard_iter(self, node: ast.For | ast.comprehension) -> _T_visit_return:
1✔
211
        """
212
        Converts:
213
            for x in expr
214
        to
215
            for x in _getiter_(expr)
216

217
        Also used for
218
        * list comprehensions
219
        * dict comprehensions
220
        * set comprehensions
221
        * generator expresions
222
        """
223
        node = self.node_contents_visit(node)
1✔
224

225
        if isinstance(node.target, ast.Tuple):
1✔
226
            spec = self.gen_unpack_spec(node.target)
1✔
227
            new_iter = ast.Call(
1✔
228
                func=ast.Name('_iter_unpack_sequence_', ast.Load()),
229
                args=[node.iter, spec, ast.Name('_getiter_', ast.Load())],
230
                keywords=[])
231
        else:
232
            new_iter = ast.Call(
1✔
233
                func=ast.Name("_getiter_", ast.Load()),
234
                args=[node.iter],
235
                keywords=[])
236

237
        copy_locations(new_iter, node.iter)
1✔
238
        node.iter = new_iter
1✔
239
        return node
1✔
240

241
    def is_starred(self, ob: ast.AST) -> typing.TypeGuard[ast.Starred]:
1✔
242
        # TODO: Change Type Annotation to typing.TypeIs[ast.Starred] when
243
        #               Support for Python 3.12 is dropped.
244
        return isinstance(ob, ast.Starred)
1✔
245

246
    def gen_unpack_spec(self, tpl: ast.Tuple) -> ast.Dict:
1✔
247
        """Generate a specification for 'guarded_unpack_sequence'.
248

249
        This spec is used to protect sequence unpacking.
250
        The primary goal of this spec is to tell which elements in a sequence
251
        are sequences again. These 'child' sequences have to be protected
252
        again.
253

254
        For example there is a sequence like this:
255
            (a, (b, c), (d, (e, f))) = g
256

257
        On a higher level the spec says:
258
            - There is a sequence of len 3
259
            - The element at index 1 is a sequence again with len 2
260
            - The element at index 2 is a sequence again with len 2
261
              - The element at index 1 in this subsequence is a sequence again
262
                with len 2
263

264
        With this spec 'guarded_unpack_sequence' does something like this for
265
        protection (len checks are omitted):
266

267
            t = list(_getiter_(g))
268
            t[1] = list(_getiter_(t[1]))
269
            t[2] = list(_getiter_(t[2]))
270
            t[2][1] = list(_getiter_(t[2][1]))
271
            return t
272

273
        The 'real' spec for the case above is then:
274
            spec = {
275
                'min_len': 3,
276
                'childs': (
277
                    (1, {'min_len': 2, 'childs': ()}),
278
                    (2, {
279
                            'min_len': 2,
280
                            'childs': (
281
                                (1, {'min_len': 2, 'childs': ()})
282
                            )
283
                        }
284
                    )
285
                )
286
            }
287

288
        So finally the assignment above is converted into:
289
            (a, (b, c), (d, (e, f))) = guarded_unpack_sequence(g, spec)
290
        """
291
        spec = ast.Dict(keys=[], values=[])
1✔
292

293
        spec.keys.append(ast.Constant('childs'))
1✔
294
        val0 = ast.Tuple([], ast.Load())
1✔
295
        spec.values.append(val0)
1✔
296

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

303
        for idx, val in enumerate(tpl.elts):
1✔
304
            # After a starred element specify the child index from the back.
305
            # Since it is unknown how many elements from the sequence are
306
            # consumed by the starred element.
307
            # For example a, *b, (c, d) = g
308
            # Then (c, d) has the index '-1'
309
            if self.is_starred(val):
1✔
310
                offset = min_len + 1
1✔
311

312
            elif isinstance(val, ast.Tuple):
1✔
313
                el = ast.Tuple([], ast.Load())
1✔
314
                el.elts.append(ast.Constant(idx - offset))
1✔
315
                el.elts.append(self.gen_unpack_spec(val))
1✔
316
                val0.elts.append(el)
1✔
317

318
        spec.keys.append(ast.Constant('min_len'))
1✔
319
        spec.values.append(ast.Constant(min_len))
1✔
320

321
        return spec
1✔
322

323
    def protect_unpack_sequence(
1✔
324
            self,
325
            target: ast.Tuple,
326
            value: ast.expr) -> ast.Call:
327
        spec = self.gen_unpack_spec(target)
1✔
328
        return ast.Call(
1✔
329
            func=ast.Name('_unpack_sequence_', ast.Load()),
330
            args=[value, spec, ast.Name('_getiter_', ast.Load())],
331
            keywords=[])
332

333
    def gen_unpack_wrapper(self,
1✔
334
                           node: ast.stmt,
335
                           target: ast.Tuple) -> tuple[ast.Name, ast.Try]:
336
        """Helper function to protect tuple unpacks.
337

338
        node: used to copy the locations for the new nodes.
339
        target: is the tuple which must be protected.
340

341
        It returns a tuple with two element.
342

343
        Element 1: Is a temporary name node which must be used to
344
                   replace the target.
345
                   The context (store, param) is defined
346
                   by the 'ctx' parameter..
347

348
        Element 2: Is a try .. finally where the body performs the
349
                   protected tuple unpack of the temporary variable
350
                   into the original target.
351
        """
352

353
        # Generate a tmp name to replace the tuple with.
354
        tmp_name = self.gen_tmp_name()
1✔
355

356
        # Generates an expressions which protects the unpack.
357
        # converter looks like 'wrapper(tmp_name)'.
358
        # 'wrapper' takes care to protect sequence unpacking with _getiter_.
359
        converter = self.protect_unpack_sequence(
1✔
360
            target,
361
            ast.Name(tmp_name, ast.Load()))
362

363
        # Assign the expression to the original names.
364
        # Cleanup the temporary variable.
365
        # Generates:
366
        # try:
367
        #     # converter is 'wrapper(tmp_name)'
368
        #     arg = converter
369
        # finally:
370
        #     del tmp_arg
371
        try_body: list[ast.stmt] = [ast.Assign(
1✔
372
            targets=[target], value=converter)]
373
        finalbody: list[ast.stmt] = [self.gen_del_stmt(tmp_name)]
1✔
374
        cleanup = ast.Try(
1✔
375
            body=try_body, finalbody=finalbody, handlers=[], orelse=[])
376

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

380
        copy_locations(tmp_target, node)
1✔
381
        copy_locations(cleanup, node)
1✔
382

383
        return (tmp_target, cleanup)
1✔
384

385
    def gen_none_node(self) -> ast.Constant:
1✔
386
        return ast.Constant(None)
1✔
387

388
    def gen_del_stmt(self, name_to_del: str) -> ast.Delete:
1✔
389
        return ast.Delete(targets=[ast.Name(name_to_del, ast.Del())])
1✔
390

391
    def check_name(
1✔
392
            self,
393
            node: _T_pos_ast,
394
            name: str | None,
395
            allow_magic_methods: bool = False) -> None:
396
        """Check names if they are allowed.
397

398
        If ``allow_magic_methods is True`` names in `ALLOWED_FUNC_NAMES`
399
        are additionally allowed although their names start with `_`.
400

401
        """
402
        if name is None:
1✔
403
            return
1✔
404

405
        if (name.startswith('_')
1✔
406
                and name != '_'
407
                and not (allow_magic_methods
408
                         and name in ALLOWED_FUNC_NAMES
409
                         and node.col_offset != 0)):
410
            self.error(
1✔
411
                node,
412
                '"{name}" is an invalid variable name because it '
413
                'starts with "_"'.format(name=name))
414
        elif name.endswith('__roles__'):
1✔
415
            self.error(node, '"%s" is an invalid variable name because '
1✔
416
                       'it ends with "__roles__".' % name)
417
        elif name in FORBIDDEN_FUNC_NAMES:
1✔
418
            self.error(node, f'"{name}" is a reserved name.')
1✔
419

420
    def check_function_argument_names(
1✔
421
            self,
422
            node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda) -> None:
423
        for arg in node.args.args:
1✔
424
            self.check_name(node, arg.arg)
1✔
425

426
        if node.args.vararg:
1✔
427
            self.check_name(node, node.args.vararg.arg)
1✔
428

429
        if node.args.kwarg:
1✔
430
            self.check_name(node, node.args.kwarg.arg)
1✔
431

432
        for arg in node.args.kwonlyargs:
1✔
433
            self.check_name(node, arg.arg)
1✔
434

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

438
        This is a protection against rebinding dunder names like
439
        _getitem_, _write_ via imports.
440

441
        => 'from _a import x' is ok, because '_a' is not added to the scope.
442
        """
443
        for name in node.names:
1✔
444
            if '*' in name.name:
1✔
445
                self.error(node, '"*" imports are not allowed.')
1✔
446
            self.check_name(node, name.name)
1✔
447
            if name.asname:
1✔
448
                self.check_name(node, name.asname)
1✔
449

450
        return self.node_contents_visit(node)
1✔
451

452
    def inject_print_collector(
1✔
453
            self,
454
            node: ast.Module | ast.FunctionDef,
455
            position: int = 0) -> None:
456
        print_used = self.print_info.print_used
1✔
457
        printed_used = self.print_info.printed_used
1✔
458

459
        if print_used or printed_used:
1✔
460
            # Add '_print = _print_(_getattr_)' add the top of a
461
            # function/module.
462
            _print = ast.Assign(
1✔
463
                targets=[ast.Name('_print', ast.Store())],
464
                value=ast.Call(
465
                    func=ast.Name("_print_", ast.Load()),
466
                    args=[ast.Name("_getattr_", ast.Load())],
467
                    keywords=[]))
468

469
            if isinstance(node, ast.Module):
1✔
470
                _print.lineno = position
1✔
471
                _print.col_offset = position
1✔
472
                _print.end_lineno = position
1✔
473
                _print.end_col_offset = position
1✔
474
                ast.fix_missing_locations(_print)
1✔
475
            else:
476
                copy_locations(_print, node)
1✔
477

478
            node.body.insert(position, _print)
1✔
479

480
            if not printed_used:
1✔
481
                self.warn(node, "Prints, but never reads 'printed' variable.")
1✔
482

483
            elif not print_used:
1✔
484
                self.warn(node, "Doesn't print, but reads 'printed' variable.")
1✔
485

486
    # Special Functions for an ast.NodeTransformer
487

488
    def generic_visit(self,  # type: ignore[override]
1✔
489
                      node: ast.AST) -> _T_visit_return:
490
        """Reject ast nodes which do not have a corresponding `visit_` method.
491

492
        This is needed to prevent new ast nodes from new Python versions to be
493
        trusted before any security review.
494

495
        To access `generic_visit` on the super class use `node_contents_visit`.
496
        """
497
        self.warn(
1✔
498
            node,
499
            '{0.__class__.__name__}'
500
            ' statement is not known to RestrictedPython'.format(node)
501
        )
502
        self.not_allowed(node)
1✔
503

504
    def not_allowed(self, node: ast.AST) -> None:
1✔
505
        self.error(
1✔
506
            node,
507
            f'{node.__class__.__name__} statements are not allowed.')
508

509
    def node_contents_visit(self, node: _T) -> _T:
1✔
510
        """Visit the contents of a node."""
511
        return super().generic_visit(node)  # type: ignore[return-value]
1✔
512

513
    # ast for Literals
514

515
    def visit_Constant(self, node: ast.Constant) -> _T_visit_return:
1✔
516
        """Allow constant literals.
517

518
        Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in
519
        Python 3.8+.
520
        :see: https://docs.python.org/dev/whatsnew/3.8.html#deprecated
521
        """
522
        return self.node_contents_visit(node)
1✔
523

524
    def visit_Interactive(self, node: ast.Interactive) -> _T_visit_return:
1✔
525
        """Allow single mode without restrictions."""
526
        return self.node_contents_visit(node)
1✔
527

528
    def visit_List(self, node: ast.List) -> _T_visit_return:
1✔
529
        """Allow list literals without restrictions."""
530
        return self.node_contents_visit(node)
1✔
531

532
    def visit_Tuple(self, node: ast.Tuple) -> _T_visit_return:
1✔
533
        """Allow tuple literals without restrictions."""
534
        return self.node_contents_visit(node)
1✔
535

536
    def visit_Set(self, node: ast.Set) -> _T_visit_return:
1✔
537
        """Allow set literals without restrictions."""
538
        return self.node_contents_visit(node)
1✔
539

540
    def visit_Dict(self, node: ast.Dict) -> _T_visit_return:
1✔
541
        """Allow dict literals without restrictions."""
542
        return self.node_contents_visit(node)
1✔
543

544
    def visit_FormattedValue(
1✔
545
            self,
546
            node: ast.FormattedValue) -> _T_visit_return:
547
        """Allow f-strings without restrictions."""
548
        return self.node_contents_visit(node)
1✔
549

550
    def visit_TemplateStr(self, node: ast.AST) -> _T_visit_return:
1✔
551
        """Template strings are allowed by default.
552

553
        As Template strings are a very basic template mechanism, that needs
554
        additional rendering logic to be useful, they are not blocked by
555
        default.
556
        Those rendering logic would be affected by RestrictedPython as well.
557

558
        TODO: Change Type Annotation to ast.TemplateStr when
559
              Support for Python 3.13 is dropped.
560
        """
UNCOV
561
        return self.node_contents_visit(node)
×
562

563
    def visit_Interpolation(self, node: ast.AST) -> _T_visit_return:
1✔
564
        """Interpolations are allowed by default.
565

566
        As Interpolations are part of Template Strings, they are needed
567
        to be reached in the context of RestrictedPython as Template Strings
568
        are allowed. As a user has to provide additional rendering logic
569
        to make use of Template Strings, the security implications of
570
        Interpolations are limited in the context of RestrictedPython.
571

572
        TODO: Change Type Annotation to ast.Interpolation when
573
              Support for Python 3.13 is dropped.
574
        """
UNCOV
575
        return self.node_contents_visit(node)
×
576

577
    def visit_JoinedStr(self, node: ast.JoinedStr) -> _T_visit_return:
1✔
578
        """Allow joined string without restrictions."""
579
        return self.node_contents_visit(node)
1✔
580

581
    # ast for Variables
582

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

642
    # Expressions
643

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

814
        needs_wrap = False
1✔
815

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

898
    # Subscripting
899

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

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

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

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

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

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

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

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

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

945
    def visit_Slice(self, node: ast.Slice) -> _T_visit_return:
1✔
946
        """
947

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

951
    # Comprehensions
952

953
    def visit_ListComp(self, node: ast.ListComp) -> _T_visit_return:
1✔
954
        """
955

956
        """
957
        return self.node_contents_visit(node)
1✔
958

959
    def visit_SetComp(self, node: ast.SetComp) -> _T_visit_return:
1✔
960
        """
961

962
        """
963
        return self.node_contents_visit(node)
1✔
964

965
    def visit_GeneratorExp(self, node: ast.GeneratorExp) -> _T_visit_return:
1✔
966
        """
967

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

971
    def visit_DictComp(self, node: ast.DictComp) -> _T_visit_return:
1✔
972
        """
973

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

977
    def visit_comprehension(self, node: ast.comprehension) -> _T_visit_return:
1✔
978
        """
979

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

983
    # Statements
984

985
    def visit_Assign(self, node: ast.Assign) -> _T_visit_return:
1✔
986
        """
987

988
        """
989

990
        node = self.node_contents_visit(node)
1✔
991

992
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
993
            return node
1✔
994

995
        # Handle sequence unpacking.
996
        # For briefness this example omits cleanup of the temporary variables.
997
        # Check 'transform_tuple_assign' how its done.
998
        #
999
        # - Single target (with nested support)
1000
        # (a, (b, (c, d))) = <exp>
1001
        # is converted to
1002
        # (a, t1) = _getiter_(<exp>)
1003
        # (b, t2) = _getiter_(t1)
1004
        # (c, d) = _getiter_(t2)
1005
        #
1006
        # - Multi targets
1007
        # (a, b) = (c, d) = <exp>
1008
        # is converted to
1009
        # (c, d) = _getiter_(<exp>)
1010
        # (a, b) = _getiter_(<exp>)
1011
        # Why is this valid ? The original bytecode for this multi targets
1012
        # behaves the same way.
1013

1014
        # ast.NodeTransformer works with list results.
1015
        # He injects it at the right place of the node's parent statements.
1016
        new_nodes = []
1✔
1017

1018
        # python fills the right most target first.
1019
        for target in reversed(node.targets):
1✔
1020
            if isinstance(target, ast.Tuple):
1✔
1021
                wrapper = ast.Assign(
1✔
1022
                    targets=[target],
1023
                    value=self.protect_unpack_sequence(target, node.value))
1024
                new_nodes.append(wrapper)
1✔
1025
            else:
1026
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1027
                new_nodes.append(new_node)
1✔
1028

1029
        for new_node in new_nodes:
1✔
1030
            copy_locations(new_node, node)
1✔
1031

1032
        return new_nodes
1✔
1033

1034
    def visit_AugAssign(self, node: ast.AugAssign) -> _T_visit_return:
1✔
1035
        """Forbid certain kinds of AugAssign
1036

1037
        According to the language reference (and ast.c) the following nodes
1038
        are are possible:
1039
        Name, Attribute, Subscript
1040

1041
        Note that although augmented assignment of attributes and
1042
        subscripts is disallowed, augmented assignment of names (such
1043
        as 'n += 1') is allowed.
1044
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1045
        """
1046

1047
        node = self.node_contents_visit(node)
1✔
1048

1049
        if isinstance(node.target, ast.Attribute):
1✔
1050
            self.error(
1✔
1051
                node,
1052
                "Augmented assignment of attributes is not allowed.")
1053
            return node
1✔
1054

1055
        elif isinstance(node.target, ast.Subscript):
1✔
1056
            self.error(
1✔
1057
                node,
1058
                "Augmented assignment of object items "
1059
                "and slices is not allowed.")
1060
            return node
1✔
1061

1062
        elif isinstance(node.target, ast.Name):
1✔
1063
            new_node = ast.Assign(
1✔
1064
                targets=[node.target],
1065
                value=ast.Call(
1066
                    func=ast.Name('_inplacevar_', ast.Load()),
1067
                    args=[
1068
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1069
                        ast.Name(node.target.id, ast.Load()),
1070
                        node.value
1071
                    ],
1072
                    keywords=[]))
1073

1074
            copy_locations(new_node, node)
1✔
1075
            return new_node
1✔
1076
        else:  # pragma: no cover
1077
            # Impossible Case - Only Node Types:
1078
            # * Name
1079
            # * Attribute
1080
            # * Subscript
1081
            # defined, those are checked before.
1082
            raise NotImplementedError(
1083
                f"Unknown target type: {type(node.target)}")
1084

1085
    def visit_Raise(self, node: ast.Raise) -> _T_visit_return:
1✔
1086
        """Allow `raise` statements without restrictions."""
1087
        return self.node_contents_visit(node)
1✔
1088

1089
    def visit_Assert(self, node: ast.Assert) -> _T_visit_return:
1✔
1090
        """Allow assert statements without restrictions."""
1091
        return self.node_contents_visit(node)
1✔
1092

1093
    def visit_Delete(self, node: ast.Delete) -> _T_visit_return:
1✔
1094
        """Allow `del` statements without restrictions."""
1095
        return self.node_contents_visit(node)
1✔
1096

1097
    def visit_Pass(self, node: ast.Pass) -> _T_visit_return:
1✔
1098
        """Allow `pass` statements without restrictions."""
1099
        return self.node_contents_visit(node)
1✔
1100

1101
    # Imports
1102

1103
    def visit_Import(self, node: ast.Import) -> _T_visit_return:
1✔
1104
        """Allow `import` statements with restrictions.
1105
        See check_import_names."""
1106
        return self.check_import_names(node)
1✔
1107

1108
    def visit_ImportFrom(self, node: ast.ImportFrom) -> _T_visit_return:
1✔
1109
        """Allow `import from` statements with restrictions.
1110
        See check_import_names."""
1111
        return self.check_import_names(node)
1✔
1112

1113
    def visit_alias(self, node: ast.alias) -> _T_visit_return:
1✔
1114
        """Allow `as` statements in import and import from statements."""
1115
        return self.node_contents_visit(node)
1✔
1116

1117
    # Control flow
1118

1119
    def visit_If(self, node: ast.If) -> _T_visit_return:
1✔
1120
        """Allow `if` statements without restrictions."""
1121
        return self.node_contents_visit(node)
1✔
1122

1123
    def visit_For(self, node: ast.For) -> _T_visit_return:
1✔
1124
        """Allow `for` statements with some restrictions."""
1125
        return self.guard_iter(node)
1✔
1126

1127
    def visit_While(self, node: ast.While) -> _T_visit_return:
1✔
1128
        """Allow `while` statements."""
1129
        return self.node_contents_visit(node)
1✔
1130

1131
    def visit_Break(self, node: ast.Break) -> _T_visit_return:
1✔
1132
        """Allow `break` statements without restrictions."""
1133
        return self.node_contents_visit(node)
1✔
1134

1135
    def visit_Continue(self, node: ast.Continue) -> _T_visit_return:
1✔
1136
        """Allow `continue` statements without restrictions."""
1137
        return self.node_contents_visit(node)
1✔
1138

1139
    def visit_Try(self, node: ast.Try) -> _T_visit_return:
1✔
1140
        """Allow `try` without restrictions."""
1141
        return self.node_contents_visit(node)
1✔
1142

1143
    def visit_TryStar(self, node: ast.AST) -> _T_visit_return:
1✔
1144
        """Disallow `ExceptionGroup` due to a potential sandbox escape.
1145

1146
        TODO: Type Annotation for node when dropping support
1147
              for Python < 3.11 should be ast.TryStar.
1148
        """
1149
        self.not_allowed(node)
1✔
1150

1151
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> _T_visit_return:
1✔
1152
        """Protect exception handlers."""
1153
        node = self.node_contents_visit(node)
1✔
1154
        self.check_name(node, node.name)
1✔
1155
        return node
1✔
1156

1157
    def visit_With(self, node: ast.With) -> _T_visit_return:
1✔
1158
        """Protect tuple unpacking on with statements."""
1159
        node = self.node_contents_visit(node)
1✔
1160

1161
        for item in reversed(node.items):
1✔
1162
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1163
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1164
                    node,
1165
                    item.optional_vars)
1166

1167
                item.optional_vars = tmp_target
1✔
1168
                node.body.insert(0, unpack)
1✔
1169

1170
        return node
1✔
1171

1172
    def visit_withitem(self, node: ast.withitem) -> _T_visit_return:
1✔
1173
        """Allow `with` statements (context managers) without restrictions."""
1174
        return self.node_contents_visit(node)
1✔
1175

1176
    # Function and class definitions
1177

1178
    def visit_FunctionDef(self, node: ast.FunctionDef) -> _T_visit_return:
1✔
1179
        """Allow function definitions (`def`) with some restrictions."""
1180
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1181
        self.check_function_argument_names(node)
1✔
1182

1183
        with self.print_info.new_print_scope():
1✔
1184
            node = self.node_contents_visit(node)
1✔
1185
            self.inject_print_collector(node)
1✔
1186
        return node
1✔
1187

1188
    def visit_Lambda(self, node: ast.Lambda) -> _T_visit_return:
1✔
1189
        """Allow lambda with some restrictions."""
1190
        self.check_function_argument_names(node)
1✔
1191
        return self.node_contents_visit(node)
1✔
1192

1193
    def visit_arguments(self, node: ast.arguments) -> _T_visit_return:
1✔
1194
        """
1195

1196
        """
1197
        return self.node_contents_visit(node)
1✔
1198

1199
    def visit_arg(self, node: ast.arg) -> _T_visit_return:
1✔
1200
        """
1201

1202
        """
1203
        return self.node_contents_visit(node)
1✔
1204

1205
    def visit_Return(self, node: ast.Return) -> _T_visit_return:
1✔
1206
        """Allow `return` statements without restrictions."""
1207
        return self.node_contents_visit(node)
1✔
1208

1209
    def visit_Yield(self, node: ast.Yield) -> _T_visit_return:
1✔
1210
        """Allow `yield`statements without restrictions."""
1211
        return self.node_contents_visit(node)
1✔
1212

1213
    def visit_YieldFrom(self, node: ast.YieldFrom) -> _T_visit_return:
1✔
1214
        """Allow `yield`statements without restrictions."""
1215
        return self.node_contents_visit(node)
1✔
1216

1217
    def visit_Global(self, node: ast.Global) -> _T_visit_return:
1✔
1218
        """Allow `global` statements without restrictions."""
1219
        return self.node_contents_visit(node)
1✔
1220

1221
    def visit_Nonlocal(self, node: ast.Nonlocal) -> _T_visit_return:
1✔
1222
        """Deny `nonlocal` statements."""
1223
        self.not_allowed(node)
1✔
1224

1225
    def visit_ClassDef(self, node: ast.ClassDef) -> _T_visit_return:
1✔
1226
        """Check the name of a class definition."""
1227
        self.check_name(node, node.name)
1✔
1228
        node = self.node_contents_visit(node)
1✔
1229
        if any(keyword.arg == 'metaclass' for keyword in node.keywords):
1✔
1230
            self.error(
1✔
1231
                node, 'The keyword argument "metaclass" is not allowed.')
1232
        CLASS_DEF = textwrap.dedent('''\
1✔
1233
            class {0.name}(metaclass=__metaclass__):
1234
                pass
1235
        '''.format(node))
1236
        new_class_node = typing.cast(
1✔
1237
            ast.ClassDef, ast.parse(CLASS_DEF).body[0])
1238
        new_class_node.body = node.body
1✔
1239
        new_class_node.bases = node.bases
1✔
1240
        new_class_node.decorator_list = node.decorator_list
1✔
1241
        return new_class_node
1✔
1242

1243
    def visit_Module(self, node: ast.Module) -> _T_visit_return:
1✔
1244
        """Add the print_collector (only if print is used) at the top."""
1245
        node = self.node_contents_visit(node)
1✔
1246

1247
        # Inject the print collector after 'from __future__ import ....'
1248
        position = 0
1✔
1249
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1250
            if not isinstance(child, ast.ImportFrom):
1✔
1251
                break
1✔
1252

1253
            if not child.module == '__future__':
1✔
1254
                break
1✔
1255

1256
        self.inject_print_collector(node, position)
1✔
1257
        return node
1✔
1258

1259
    # Async und await
1260

1261
    def visit_AsyncFunctionDef(
1✔
1262
            self, node: ast.AsyncFunctionDef) -> _T_visit_return:
1263
        """Deny async functions."""
1264
        self.not_allowed(node)
1✔
1265

1266
    def visit_Await(self, node: ast.Await) -> _T_visit_return:
1✔
1267
        """Deny async functionality."""
1268
        self.not_allowed(node)
1✔
1269

1270
    def visit_AsyncFor(self, node: ast.AsyncFor) -> _T_visit_return:
1✔
1271
        """Deny async functionality."""
1272
        self.not_allowed(node)
1✔
1273

1274
    def visit_AsyncWith(self, node: ast.AsyncWith) -> _T_visit_return:
1✔
1275
        """Deny async functionality."""
1276
        self.not_allowed(node)
1✔
1277

1278
    # Assignment expressions (walrus operator ``:=``)
1279
    # New in 3.8
1280
    def visit_NamedExpr(self, node: ast.NamedExpr) -> _T_visit_return:
1✔
1281
        """Allow assignment expressions under some circumstances."""
1282
        # while the grammar requires ``node.target`` to be a ``Name``
1283
        # the abstract syntax is more permissive and allows an ``expr``.
1284
        # We support only a ``Name``.
1285
        # This is safe as the expression can only add/modify local
1286
        # variables. While this may hide global variables, an
1287
        # (implicitly performed) name check guarantees (as usual)
1288
        # that no essential global variable is hidden.
1289
        node = self.node_contents_visit(node)  # this checks ``node.target``
1✔
1290
        target = node.target
1✔
1291
        if not isinstance(target, ast.Name):
1✔
1292
            self.error(  # type: ignore[unreachable]
1✔
1293
                node,
1294
                "Assignment expressions are only allowed for simple targets")
1295
        return node
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc