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

zopefoundation / RestrictedPython / 18631857581

19 Oct 2025 02:34PM UTC coverage: 97.98%. First build
18631857581

Pull #303

github

dataflake
- fix last test
Pull Request #303: Type Annotations for RestrictedPython

214 of 233 branches covered (91.85%)

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

2522 of 2574 relevant lines covered (97.98%)

0.98 hits per line

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

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

114

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

122
    assert 'end_lineno' in new_node._attributes
1✔
123
    new_node.end_lineno = old_node.end_lineno
1✔
124

125
    assert 'col_offset' in new_node._attributes
1✔
126
    new_node.col_offset = old_node.col_offset
1✔
127

128
    assert 'end_col_offset' in new_node._attributes
1✔
129
    new_node.end_col_offset = old_node.end_col_offset
1✔
130

131
    ast.fix_missing_locations(new_node)
1✔
132

133

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

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

144
        self.print_used = False
1✔
145
        self.printed_used = False
1✔
146

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

153

154
class RestrictingNodeTransformer(ast.NodeTransformer):
1✔
155

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

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

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

175
        self.print_info = PrintInfo()
1✔
176

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

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

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

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

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

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

223
        copy_locations(new_iter, node.iter)
1✔
224
        node.iter = new_iter
1✔
225
        return node
1✔
226

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

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

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

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

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

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

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

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

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

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

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

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

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

301
        spec.keys.append(ast.Constant('min_len'))
1✔
302
        spec.values.append(ast.Constant(min_len))
1✔
303

304
        return spec
1✔
305

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

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

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

323
        It returns a tuple with two element.
324

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

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

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

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

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

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

361
        copy_locations(tmp_target, node)
1✔
362
        copy_locations(cleanup, node)
1✔
363

364
        return (tmp_target, cleanup)
1✔
365

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

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

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

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

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

384
        elif isinstance(slice_, ast.Index):
×
385
            return slice_.value
×
386

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

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

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

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

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

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

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

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

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

431
        """
432
        if name is None:
1✔
433
            return
1✔
434

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

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

454
        if node.args.vararg:
1✔
455
            self.check_name(node, node.args.vararg.arg)
1✔
456

457
        if node.args.kwarg:
1✔
458
            self.check_name(node, node.args.kwarg.arg)
1✔
459

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

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

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

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

478
        return self.node_contents_visit(node)
1✔
479

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

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

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

503
            node.body.insert(position, _print)
1✔
504

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

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

511
    # Special Functions for an ast.NodeTransformer
512

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

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

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

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

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

537
    # ast for Literals
538

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

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

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

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

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

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

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

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

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

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

588
        TODO: Change Type Annotation to ast.TemplateStr when
589
              Support for Python 3.13 is dropped.
590
        """
591
        return self.node_contents_visit(node)
×
592

593
    def visit_Interpolation(self, node):
1✔
594
        """Interpolations are allowed by default.
595

596
        As Interpolations are part of Template Strings, they are needed
597
        to be reached in the context of RestrictedPython as Template Strings
598
        are allowed. As a user has to provide additional rendering logic
599
        to make use of Template Strings, the security implications of
600
        Interpolations are limited in the context of RestrictedPython.
601

602
        TODO: Change Type Annotation to ast.Interpolation when
603
              Support for Python 3.13 is dropped.
604
        """
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
        """
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
        """
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