Thom Smith
Thom Smith

Reputation: 14086

Using ast to create a function with a keyword-only argument that has a default value

I'm trying to use ast to dynamically create a function with a keyword-only argument that has a default value. However, the resulting function still requires the argument and will raise TypeError if it's not passed.

This is the code creating the function f:

import ast
import types

module_ast = ast.Module(
    body=[
        ast.FunctionDef(
            name='f',
            args=ast.arguments(
                args=[],
                vararg=None,
                kwarg=None,
                defaults=[],
                kwonlyargs=[
                    ast.arg(
                        arg='x',
                        lineno=1,
                        col_offset=0,
                    ),
                ],
                kw_defaults=[
                    ast.Num(n=42, lineno=1, col_offset=0),
                ],
                posonlyargs=[],
            ),
            body=[ast.Return(
                value=ast.Name(id='x', ctx=ast.Load(), lineno=1, col_offset=0),
                lineno=1,
                col_offset=0,
            )],
            decorator_list=[],
            lineno=1,
            col_offset=0,
        )
    ],
    type_ignores=[],
)

module_code = compile(module_ast, '<ast>', 'exec')

# This is the part that I'm suspicious of
f_code = next(c for c in module_code.co_consts if isinstance(c, types.CodeType))

f = types.FunctionType(
    f_code,
    {}
)

If I print(ast.unparse(module_ast)), I get what I expect:

def f(*, x=42):
    return x

Calling f(x=100) returns 100 as expected, but calling f() produces:

TypeError: f() missing 1 required keyword-only argument: 'x'

I suspect that the problem is in way the I'm turning the AST into a function. I saw the approach in another question here (which I unfortunately do not have a link to). It looks a bit dodgy, but I'm not sure how else to do it.

Upvotes: 1

Views: 525

Answers (1)

user2357112
user2357112

Reputation: 280857

A function's default argument values aren't part of the function's code object. They can't be, because the defaults are created at function definition time, not bytecode compilation time. Default argument values are stored in __defaults__ for non-keyword-only arguments and __kwdefaults__ for keyword-only arguments. When you extract f_code from module_code, you're not getting any information about defaults.

Execute the function definition, then retrieve the actual function object:

namespace = {}
exec(module_code, namespace)
function = namespace['f']

Upvotes: 3

Related Questions