HelloWorld
HelloWorld

Reputation: 1863

Get parameter(arg) count of builtin functions in Python

I wrote my own c-module for Python and for a custom table in a documentation I need the number of parameters of the builtin-functions during runtime.

There are functions in Python 2 like inspect.getargspec or functions in Python 3 like inspect.signature which support normal Python functions, but leave builtin-functions unsupported.

There are two other community solutions so far:

In some cases the docstrings are outdated and/or it's hard to extract the argument count since the docstring can be any plain string. Parsing the original *.c file is a good approach as well, but you might not have access to it.

Upvotes: 4

Views: 1388

Answers (1)

HelloWorld
HelloWorld

Reputation: 1863

In the following this is the working solution I came up with for Python 2 and 3.

What does it do?

During runtime a list of 99 None objects gets passed to the corresponding function. One of the first checks in the internal parsing function PyArg_ParseTuple checks if the amount of parameters matches the amount of passed parameters - if not it will fail. That means we will call the function but we can also be sure it doesn't get really executed.

Technical background:

Why is it so hard to get the count of parameters of built-in functions? The problem is that the parameter list is evaluated during runtime, not compile time. A very simple example of a built-in function in C looks like this:

static PyObject* example(PyObject *self, PyObject *args)
{
    int myFirstParam;
    if(!PyArg_ParseTuple(args, "i", &myFirstParam))
        return NULL;
    ...
}

Copy and Paste Solution:

import inspect
import time
import re
import types
import sys


def get_parameter_count(func):
    """Count parameter of a function.

    Supports Python functions (and built-in functions).
    If a function takes *args, then -1 is returned

    Example:
        import os
        arg = get_parameter_count(os.chdir)
        print(arg)  # Output: 1

    -- For C devs:
    In CPython, some built-in functions defined in C provide
    no metadata about their arguments. That's why we pass a
    list with 999 None objects (randomly choosen) to it and
    expect the underlying PyArg_ParseTuple fails with a
    corresponding error message.
    """

    # If the function is a builtin function we use our
    # approach. If it's an ordinary Python function we
    # fallback by using the the built-in extraction
    # functions (see else case), otherwise
    if isinstance(func, types.BuiltinFunctionType):
        try:
            arg_test = 999
            s = [None] * arg_test
            func(*s)
        except TypeError as e:
            message = str(e)
            found = re.match(
                r"[\w]+\(\) takes ([0-9]{1,3}) positional argument[s]* but " +
                str(arg_test) + " were given", message)
            if found:
                return int(found.group(1))

            if "takes no arguments" in message:
                return 0
            elif "takes at most" in message:
                found = re.match(
                    r"[\w]+\(\) takes at most ([0-9]{1,3}).+", message)
                if found:
                    return int(found.group(1))
            elif "takes exactly" in message:
                # string can contain 'takes 1' or 'takes one',
                # depending on the Python version
                found = re.match(
                    r"[\w]+\(\) takes exactly ([0-9]{1,3}|[\w]+).+", message)
                if found:
                    return 1 if found.group(1) == "one" \
                            else int(found.group(1))
        return -1  # *args
    else:
        try:
            if (sys.version_info > (3, 0)):
                argspec = inspect.getfullargspec(func)
            else:
                argspec = inspect.getargspec(func)
        except:
            raise TypeError("unable to determine parameter count")

        return -1 if argspec.varargs else len(argspec.args)



def print_get_parameter_count(mod):
    for x in dir(mod):
        e = mod.__dict__.get(x)
        if isinstance(e, types.BuiltinFunctionType):
            print("{}.{} takes {} argument(s)".format(mod.__name__, e.__name__, get_parameter_count(e)))

import os
print_get_parameter_count(os)

Output:

os._exit takes 1 argument(s)
os.abort takes 0 argument(s)
os.access takes 2 argument(s)
os.chdir takes 1 argument(s)
os.chmod takes 2 argument(s)
os.close takes 1 argument(s)
...

Upvotes: 3

Related Questions