Search code examples
pythoncpython

Calling PyObject_CallMethod with a single tuple unpacks arguments?


Consider the following:

PyObject* fmt = PyUnicode_FromString("{0!r}");
PyObject* tup = PyTuple_New(2);
PyTuple_SetItem(tup, 0, PyUnicode_FromString("hello"));
PyTuple_SetItem(tup, 1, PyUnicode_FromString("world"));
PyObject* formatted = PyObject_CallMethod(fmt, "format", "O", tup);
PyObject* bytes = PyUnicode_AsEncodedString(formatted, "UTF-8", "strict");
printf(PyBytes_AS_STRING(bytes));

I expect it to act like this python code:

>>> u'{0!r}'.format((u"hello", u"world"))
"(u'hello', u'world')"

However my output is simply:

u'hello'

I can imagine it is actually calling the function like:

>>> u'{0!r}'.format(u"hello", u"world")
u'hello'

What I'm looking for:

  1. Why?
  2. What is the minimal change can I get to have my expected output?

Solution

  • The issue appears to be with the way Py_BuildValue works (which seems to be used by PyObject_CallMethod). From the docs (emphasis mine):

    Py_BuildValue() does not always build a tuple. It builds a tuple only if its format string contains two or more format units. If the format string is empty, it returns None; if it contains exactly one format unit, it returns whatever object is described by that format unit. To force it to return a tuple of size 0 or one, parenthesize the format string.

    This means that instead of building the format string "O" with tup into args=(tup,) and calling fmt.format(*args) (expanding to fmt.format(("hello", "world"))), it builds args=tup, and so fmt.format(*args) expands to fmt.format("hello", "world"), as you thought. The solution is also in the docs:

    To force it to return a tuple of size 0 or one, parenthesize the format string.

    So, just change:

    PyObject* formatted = PyObject_CallMethod(fmt, "format", "O", tup);
    

    To:

    PyObject* formatted = PyObject_CallMethod(fmt, "format", "(O)", tup);
    

    And you get the desired output of ('hello', 'world'). Full code snippet (compiled with gcc thissnippet.c -I /usr/include/python3.4m/ -l python3.4m):

    #include <Python.h>
    int main() {
        Py_Initialize();
        PyObject* fmt = PyUnicode_FromString("{0!r}");
        PyObject* tup = PyTuple_New(2);
        PyTuple_SetItem(tup, 0, PyUnicode_FromString("hello"));
        PyTuple_SetItem(tup, 1, PyUnicode_FromString("world"));
        PyObject* formatted = PyObject_CallMethod(fmt, "format", "(O)", tup);
        PyObject* bytes = PyUnicode_AsEncodedString(formatted, "UTF-8", "strict");
        printf(PyBytes_AS_STRING(bytes));
        Py_Finalize();
    }