From fd23dd7c973cc3f269f500549ad59ae5cc187fd0 Mon Sep 17 00:00:00 2001 From: zedzhen <59135268+zedzhen@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:02:39 +0300 Subject: [PATCH 1/6] update-docs --- docs/usage/api.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/usage/api.rst b/docs/usage/api.rst index 8e8ca909..2c8d1fea 100644 --- a/docs/usage/api.rst +++ b/docs/usage/api.rst @@ -67,10 +67,10 @@ API overview :param flags: (optional). defaults to ``0`` :param dont_inherit: (optional). defaults to ``False`` :param policy: (optional). defaults to ``RestrictingNodeTransformer`` - :type p: str or unicode text - :type body: str or unicode text - :type name: str or unicode text - :type filename: str or unicode text + :type p: str + :type body: str or bytes or bytearray + :type name: str + :type filename: str or bytes or os.PathLike[typing.Any] :type globalize: None or list :type flags: int :type dont_inherit: int From 0095ea5403f750e0e8681308843747f88fd13c89 Mon Sep 17 00:00:00 2001 From: zedzhen <59135268+zedzhen@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:06:51 +0300 Subject: [PATCH 2/6] update-docs --- docs/usage/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/api.rst b/docs/usage/api.rst index 2c8d1fea..59eaddd3 100644 --- a/docs/usage/api.rst +++ b/docs/usage/api.rst @@ -68,7 +68,7 @@ API overview :param dont_inherit: (optional). defaults to ``False`` :param policy: (optional). defaults to ``RestrictingNodeTransformer`` :type p: str - :type body: str or bytes or bytearray + :type body: str or bytes or bytearray or ``ast.Module`` or ``ast.Expression`` or ``ast.Interactive`` :type name: str :type filename: str or bytes or os.PathLike[typing.Any] :type globalize: None or list From c8b4e26e127520ec1afbe3924b9a2e0d203f2924 Mon Sep 17 00:00:00 2001 From: zedzhen <59135268+zedzhen@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:20:43 +0300 Subject: [PATCH 3/6] add support ast --- src/RestrictedPython/compile.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/RestrictedPython/compile.py b/src/RestrictedPython/compile.py index 3253b8c9..d32ab603 100644 --- a/src/RestrictedPython/compile.py +++ b/src/RestrictedPython/compile.py @@ -4,6 +4,7 @@ from RestrictedPython._compat import IS_CPYTHON from RestrictedPython.transformer import RestrictingNodeTransformer +from RestrictedPython.transformer import copy_locations CompileResult = namedtuple( @@ -140,16 +141,24 @@ def compile_restricted_function( http://restrictedpython.readthedocs.io/en/latest/usage/index.html#RestrictedPython.compile_restricted_function """ # Parse the parameters and body, then combine them. - try: - body_ast = ast.parse(body, '', 'exec') - except SyntaxError as v: - error = syntax_error_template.format( - lineno=v.lineno, - type=v.__class__.__name__, - msg=v.msg, - statement=v.text.strip() if v.text else None) - return CompileResult( - code=None, errors=(error,), warnings=(), used_names=()) + if isinstance(body, ast.Expression): + _body_ast = ast.Expr(body.body) + copy_locations(_body_ast, body.body) + body_ast = [_body_ast] + elif isinstance(body, (ast.Module, ast.Interactive)): + body_ast = body.body + else: + try: + _body_ast = ast.parse(body, '', 'exec') + except SyntaxError as v: + error = syntax_error_template.format( + lineno=v.lineno, + type=v.__class__.__name__, + msg=v.msg, + statement=v.text.strip() if v.text else None) + return CompileResult( + code=None, errors=(error,), warnings=(), used_names=()) + body_ast = _body_ast.body # The compiled code is actually executed inside a function # (that is called when the code is called) so reading and assigning to a @@ -157,7 +166,7 @@ def compile_restricted_function( # UnboundLocalError. # We don't want the user to need to understand this. if globalize: - body_ast.body.insert(0, ast.Global(globalize)) + body_ast.insert(0, ast.Global(globalize)) wrapper_ast = ast.parse('def masked_function_name(%s): pass' % p, '', 'exec') # In case the name you chose for your generated function is not a @@ -166,7 +175,7 @@ def compile_restricted_function( assert isinstance(function_ast, ast.FunctionDef) function_ast.name = name - wrapper_ast.body[0].body = body_ast.body + wrapper_ast.body[0].body = body_ast wrapper_ast = ast.fix_missing_locations(wrapper_ast) result = _compile_restricted_mode( From 5083dbb2a1cb43f0191f4388525452b3df0e8a65 Mon Sep 17 00:00:00 2001 From: zedzhen <59135268+zedzhen@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:21:56 +0300 Subject: [PATCH 4/6] add tests --- tests/test_compile_restricted_function.py | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/test_compile_restricted_function.py b/tests/test_compile_restricted_function.py index d1454db8..b282ad44 100644 --- a/tests/test_compile_restricted_function.py +++ b/tests/test_compile_restricted_function.py @@ -1,3 +1,4 @@ +import ast from types import FunctionType from RestrictedPython import PrintCollector @@ -233,3 +234,68 @@ def test_compile_restricted_function_invalid_syntax(): assert error_msg.startswith( "Line 1: SyntaxError: cannot assign to literal here. Maybe " ) + + +def test_compile_restricted_function_pre_parse_exec(): + p = '' + body = ast.parse(""" +print("Hello World!") +return printed +""") + name = "hello_world" + global_symbols = [] + + result = compile_restricted_function( + p, # parameters + body, + name, + filename='', + globalize=global_symbols + ) + + assert result.code is not None + assert result.errors == () + + safe_globals = { + '__name__': 'script', + '_getattr_': getattr, + '_print_': PrintCollector, + '__builtins__': safe_builtins, + } + safe_locals = {} + exec(result.code, safe_globals, safe_locals) + hello_world = safe_locals['hello_world'] + assert type(hello_world) is FunctionType + assert hello_world() == 'Hello World!\n' + + +def test_compile_restricted_function_pre_parse_single(): + p = '' + body = ast.parse(""" +return "Hello World!" +""", mode="single") + name = "hello_world" + global_symbols = [] + + result = compile_restricted_function( + p, # parameters + body, + name, + filename='', + globalize=global_symbols + ) + + assert result.code is not None + assert result.errors == () + + safe_globals = { + '__name__': 'script', + '_getattr_': getattr, + '_print_': PrintCollector, + '__builtins__': safe_builtins, + } + safe_locals = {} + exec(result.code, safe_globals, safe_locals) + hello_world = safe_locals['hello_world'] + assert type(hello_world) is FunctionType + assert hello_world() == 'Hello World!' From ecdae312b36d317b1fd60b59124394fdefeadadf Mon Sep 17 00:00:00 2001 From: zedzhen <59135268+zedzhen@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:43:33 +0300 Subject: [PATCH 5/6] update changelog --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 537c5a8d..d0609a42 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,7 @@ Changes 8.3 (unreleased) ---------------- -- Nothing changed yet. +- Allow ``ast.Module``, ``ast.Expression`` and ``ast.Interactive`` as body in compile_restricted_function 8.3a1.dev0 (2026-05-29) From 3d6ff35c9d50f3760f736ae004bf3afc8bd1fb0a Mon Sep 17 00:00:00 2001 From: zedzhen <59135268+zedzhen@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:59:44 +0300 Subject: [PATCH 6/6] small fix --- src/RestrictedPython/compile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/RestrictedPython/compile.py b/src/RestrictedPython/compile.py index d32ab603..2d0513a3 100644 --- a/src/RestrictedPython/compile.py +++ b/src/RestrictedPython/compile.py @@ -149,7 +149,7 @@ def compile_restricted_function( body_ast = body.body else: try: - _body_ast = ast.parse(body, '', 'exec') + body_ast = ast.parse(body, '', 'exec').body except SyntaxError as v: error = syntax_error_template.format( lineno=v.lineno, @@ -158,7 +158,6 @@ def compile_restricted_function( statement=v.text.strip() if v.text else None) return CompileResult( code=None, errors=(error,), warnings=(), used_names=()) - body_ast = _body_ast.body # The compiled code is actually executed inside a function # (that is called when the code is called) so reading and assigning to a