Search code examples
pythoncpython-3.xpython-c-api

Partially parse args/kwargs using the Python C Api


I try to implement a Python function using the Python C API, that has a functionality similar to (this implementation is simplified and does not include things like NULL check of keywds):

def my_func(arg1, arg2 = 2, **kwargs):
  if arg2 == 1:
    # expect certain parameters to be in kwargs
  if arg2 == 2:
    # expect different parameters to be in kwargs
  if arg2 == 3:
   # call a Python function with the kwargs (whats in the kwargs is unknown)
   PythonFunc(**kwargs)

arg1 and arg2 can be both named and unnamed arguments.

The best I came up with was the following:

PyObject* my_func(PyObject* /*self*/, PyObject* args, PyObject* keywds) {
  PyObject* arg1, arg2;

  switch(PyTuple_Size(args)) {
  case 0:
    arg1 = PyDict_GetItemString(keywds, "arg1");
    if (arg2) PyDict_DelItemString(keywds, "arg1");
    else // throw TypeError

    arg2 = PyDict_GetItemString(keywds, "arg2");
    if (arg2) PyDict_DelItemString(keywds, "arg2");
    break;
  case 1:
    arg1 = PyTuple_GET_ITEM(args, 0);
    if (PyDict_GetItemString(keywds, "arg1")) // throw TypeError

    arg2 = PyDict_GetItemString(keywds, "arg2");
    if (arg2) PyDict_DelItemString(keywds, "arg2");
    break;
  case 2:
    arg1 = PyTuple_GET_ITEM(args, 0);
    if (PyDict_GetItemString(keywds, "arg1")) // throw TypeError

    arg2 = PyTuple_GET_ITEM(args, 1);
    if (PyDict_GetItemString(keywds, "arg2")) // throw TypeError
    break;
  default:
    // throw TypeError
  }

  if (...) // parse remaining keywds according to requirements
  else // call Python functions using the remaining keyword arguments
}

This can become quite extensive when the functions have more arguments. Is there a simpler way to achieve this behaviour e.g. using PyArg_ParseTupleAndKeywords?


Solution

  • You can't use PyArg_ParseTupleAndKeywords directly because it requires all the possible keyword arguments to be set. It doesn't have a "any remaining arguments are keywords" mode.

    One option to use it would be to copy/move arg1 and arg2 to another dict and feed that to PyArg_ParseTupleAndKeywords:

    int arg1;
    int arg2=2;
    char *keywords[] = {"arg1", "arg2", NULL};
    PyObject* arg1arg2dict = PyDict_New(); // error check missing...
    for (int i=0; keywords[i] != NULL; ++i) {
        PyObject* item = PyDict_GetItemString(kwds, keywords[i]);
        if (item) {
            PyDict_SetItemString(arg1arg2dict, keywords[i], item);
            PyDict_DelItemString(kwds, keywords[i]);
        }
    }
            
    if (!PyArg_ParseTupleAndKeywords(args, arg1arg2dict, "i|i:f", keywords, &arg1, &arg2)) {
        Py_DECREF(arg1arg2dict);
        return NULL;
    }
    Py_DECREF(arg1arg2dict);
    

    Obviously that's a bit long but unlike your switch statement it automates well to adding more arguments.


    Another option would be to write a loop instead of your switch statement. It's nothing too complicated and again would be easily expandable. In this version you'd avoid PyArg_ParseTupleAndKeywords (since you'd effectively have implemented a chunk of it)

    PyObject* parsed_args[] = {NULL, NULL};
       char *keywords[] = {"arg1", "arg2", NULL};
            
       int i=0;
       for (; i < PyTuple_Size(args); ++i) {
            if (keywords[i] == NULL) {
                // run out of possible positional arguments - clean up and raise error
            }
                
            if (PyDict_GetItemString(kwds, keywords[i]) {
                // clean up and raise an error
            }
                
            parsed_args[i] = PyTuple_GET_ITEM(args, i);
            Py_INCREF(parsed_args[i]);
       }
       for (; keywords[i] != NULL; ++i) {
           PyObject* item = PyDict_GetItemString(kwds, keywords[i]);
           if (item) {
               Py_INCREF(item);
               parsed_args[i] = item;
               PyDict_DelItemString(kwds, keywords[i]);
           } else {
               // raise error, goto cleanup
           }
        }
    }
    

    The cleanup is just a simple loop of Py_XDECREF over parsed_args so can be handled by a shared goto.