keflavich
keflavich

Reputation: 19205

Add methods to a class generated from other methods

I have classes like this:

class Tool(object):
    def do_async(*args):
        pass

for which I want to automatically generate non-async methods that make use of the async methods:

class Tool(object):
    def do_async(*args):
        pass
    def do(*args):
        result = self.do_async(*args)
        return magical_parser(result)

This gets to be particularly tricky because each method needs to be accessible as both an object and class method, which is normally achieved with this magical decorator:

class class_or_instance(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, obj, cls):
        if obj is not None:
            f = lambda *args, **kwds: self.fn(obj, *args, **kwds)
        else:
            f = lambda *args, **kwds: self.fn(cls, *args, **kwds)
        functools.update_wrapper(f, self.fn)
        return f

How can I make these methods, and make sure they're accessible as both class and object methods? This seems like something that could be done with decorators, but I am not sure how.

(Note that I don't know any of the method names in advance, but I know that all of the methods that need new buddies have _async at the end of their names.)

I think I've gotten fairly close, but this approach does not appropriately set the functions as class/object methods:

def process_asyncs(cls):

    methods = cls.__dict__.keys()
    for k in methods:
        methodname = k.replace("_async","")
        if 'async' in k and methodname not in methods:

            @class_or_instance
            def method(self, verbose=False, *args, **kwargs):
                response = self.__dict__[k](*args,**kwargs)
                result = self._parse_result(response, verbose=verbose)
                return result

            method.__docstr__ = ("Returns a table object.\n" +
                    cls.__dict__[k].__docstr__)

            setattr(cls,methodname,MethodType(method, None, cls))

Upvotes: 0

Views: 298

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121644

Do not get the other method from the __dict__; use getattr() instead so the descriptor protocol can kick in.

And don't wrap the method function in a MethodType() object as that'd neutralize the descriptor you put on method.

You need to bind k to the function you generate; a closured k would change with the loop:

@class_or_instance
def method(self, verbose=False, _async_method_name=k, *args, **kwargs):
    response = getattr(self, _async_method_name)(*args,**kwargs)
    result = self._parse_result(response, verbose=verbose)
    return result

cls.__dict__[methodname] = method

Don't forget to return cls at the end; I've changed this to use a separate function to create a new scope to provide a new local name _async_method_name instead of a keyword parameter; this avoids difficulties with *args and explicit keyword arguments:

def process_asyncs(cls):

    def create_method(async_method):

        @class_or_instance
        def newmethod(self, *args, **kwargs):
            if 'verbose' in kwargs:
                verbose = kwargs.pop('verbose')
            else:
                verbose = False
            response = async_method(*args,**kwargs)
            result = self._parse_result(response, verbose=verbose)
            return result
        return newmethod

    methods = cls.__dict__.keys()
    for k in methods:
        methodname = k.replace("_async","")
        if 'async' in k and methodname not in methods:
            async_method = getattr(cls, k)
            setattr(cls, methodname, create_method(async_method))

    return cls

Upvotes: 3

Related Questions