Thom Smith
Thom Smith

Reputation: 14086

Pass extra optional arguments to callbacks without breaking existing callbacks

I have an API method that accepts a callback. The callback expects one argument.

I would like this method to pass a second argument to callbacks that accept it. However, I must maintain compatibility with callbacks that accept only the original argument. (In fact, I expect that most users will not care about the additional argument, so it would be annoying to force them to explicitly ignore it.)

I know that this can be done using inspect. I'm wondering if there is an "idiomatic" or commonly used solution that's not quite so heavyweight.

Upvotes: 5

Views: 1273

Answers (3)

blhsing
blhsing

Reputation: 106455

A simpler solution would be to use a try block to try calling the callback with a second argument first, before falling back to calling with just one argument in the except block:

try:
    callback(first, second)
except TypeError as e:
    if e.__traceback__.tb_frame.f_code.co_name != 'func_name':
        raise
    callback(first)

Upvotes: 2

I think you can use __code__ to look how much arguments needed by the callback.

if callback.__code__.co_argcount == 2:
    callback(arg1, arg2)
else:
    callback(arg1)

This code isn't tested but it should work.

Upvotes: 5

Thom Smith
Thom Smith

Reputation: 14086

Using a function wrapper:

from inspect import signature, Parameter

def ignore_extra_arguments(function):
    positional_count = 0
    var_positional = False
    keyword_names = set()
    var_keyword = False

    for p in signature(function).parameters.values():
        if p.kind == Parameter.POSITIONAL_ONLY:
            positional_count += 1
        elif p.kind == Parameter.POSITIONAL_OR_KEYWORD:
            positional_count += 1
            keyword_names.add(p.name)
        elif p.kind == Parameter.VAR_POSITIONAL:
            var_positional = True
        elif p.kind == Parameter.KEYWORD_ONLY:
            keyword_names.add(p.name)
        elif p.kind == Parameter.VAR_KEYWORD:
            var_keyword = True

    if var_positional:
        new_args = lambda args: args
    else:
        new_args = lambda args: args[:positional_count]

    if var_keyword:
        new_kwargs = lambda kwargs: kwargs
    else:
        new_kwargs = lambda kwargs: {
            name: value for name, value in kwargs.items()
            if name in keyword_names
        }

    def wrapped(*args, **kwargs):
        return function(
            *new_args(args),
            **new_kwargs(kwargs)
        )

    return wrapped

It works, but it's a bit brute-force.

A simpler version, assuming that function has no keyword or variadic parameters:

from inspect import signature

def ignore_simple(function):
    count = len(signature(function).parameters)
    return lambda *args: function(*args[:count])

Upvotes: -1

Related Questions