PyEval_EvalCodeEx seem to ignore default on 3.11

Hi all,

I am trying to figure a difference in behavior between Python 3.10 and 3.11 when it comes to using PyEval_EvalCodeEx. My use case is a bit exotic, I need to call a function but control the locals passed to the function when called. I have the following helper that works fine on Python 3.10 but fails to pass a default argument on 3.11. That helper does not support kwonly arguments but that is unrelated. I tried comparing the implementation of PyEval_EvalCodeEx in 3.10 and 3.11 but did not find anything I believe relevant. For reference the project making use of this is GitHub - nucleic/enaml: Declarative User Interfaces for Python, the helper is defined in enaml/src/funchelper.c and used in enaml/src/declarative_function.cpp . Calling my test function directly succeed but using the wrapper it complains about a missing argument.

Any idea is welcome

Matthieu

PyObject*
call_func( PyObject* mod, PyObject* args )
{
    PyObject* func;
    PyObject* func_args;
    PyObject* func_kwargs;
    PyObject* func_locals = Py_None;

    if( !PyArg_UnpackTuple( args, "call_func", 3, 4, &func, &func_args, &func_kwargs, &func_locals ) )
    {
        return 0;
    }

    if( !PyFunction_Check( func ) )
    {
        PyErr_SetString( PyExc_TypeError, "function must be a Python function" );
        return 0;
    }

    if( !PyTuple_Check( func_args ) )
    {
        PyErr_SetString( PyExc_TypeError, "arguments must be a tuple" );
        return 0;
    }

    if( !PyDict_Check( func_kwargs ) )
    {
        PyErr_SetString( PyExc_TypeError, "keywords must be a dict" );
        return 0;
    }

    if( func_locals != Py_None && !PyMapping_Check( func_locals ) )
    {
        PyErr_SetString( PyExc_TypeError, "locals must be a mapping" );
        return 0;
    }
    if( func_locals == Py_None )
        func_locals = 0;

    PyObject** defaults = 0;
    Py_ssize_t num_defaults = 0;
    PyObject* argdefs = PyFunction_GET_DEFAULTS( func );
    if( ( argdefs ) && PyTuple_Check( argdefs ) )
    {
        PyObject_Print(argdefs, stdout, 0);
        defaults = &PyTuple_GET_ITEM( reinterpret_cast<PyTupleObject*>( argdefs ), 0 );
        num_defaults = PyTuple_Size( argdefs );
    }

    PyObject** keywords = 0;
    Py_ssize_t num_keywords = PyDict_Size( func_kwargs );
    if( num_keywords > 0 )
    {
        keywords = PyMem_NEW( PyObject*, 2 * num_keywords );
        if( !keywords )
        {
            PyErr_NoMemory();
            return 0;
        }
        Py_ssize_t i = 0;
        Py_ssize_t pos = 0;
        while( PyDict_Next( func_kwargs, &pos, &keywords[ i ], &keywords[ i + 1 ] ) )
            i += 2;
        num_keywords = i / 2;
        /* XXX This is broken if the caller deletes dict items! */
    }

    // XXX Support kwonly defaults
    printf("Arg number: %d, keyword number %d, default number %d\n", PyTuple_Size( func_args ), num_keywords, num_defaults);

    PyObject* result = PyEval_EvalCodeEx(
        PyFunction_GET_CODE( func ),
        PyFunction_GET_GLOBALS( func ),
        func_locals,
        &PyTuple_GET_ITEM( func_args, 0 ),
        PyTuple_Size( func_args ),
        keywords, num_keywords,
        defaults, num_defaults,
        NULL,
        PyFunction_GET_CLOSURE( func )
    );

    if( keywords )
        PyMem_DEL( keywords );

    return result;
 }