kleptocrat February 2016

What's the proper way of defining or documenting calls handled by __getattr__?

I have a class who's job is to wrap another class (code I don't control), intercept all calls to the wrapped class, perform some logic, and pass along the call to the underlying class. Here's an example:

class GithubRepository(object):

    def get_commit(self, sha):
        return 'Commit {}'.format(sha)

    def get_contributors(self):
        return ['bobbytables']


class LoggingGithubRepositoryWrapper(object):

    def __init__(self, github_repository):
        self._github_repository = github_repository

    def __getattr__(self, name):
        base_func = getattr(self._github_repository, name)

        def log_wrap(*args, **kwargs):
            print "Calling {}".format(name)
            return base_func(*args, **kwargs)

        return log_wrap

if __name__ == '__main__':

    git_client = LoggingGithubRepositoryWrapper(GithubRepository())

    print git_client.get_commit('abcdef1245')
    print git_client.get_contributors()

As you can see, the way that I do this is by implementing __getattr__ on the wrapping class and delegating to the underlying class. The downside to this approach is that users of LoggingGithubRepositoryWrapper don't know which attributes/methods the underlying GithubRepository actually has.

This leads me to my question: is there a way to define or document the calls handled by __getattr__? Ideally, I'd like to be able to autocomplete on git_client. and be provided a list of supported methods. Thanks for your help in advance!

Answers


Brendan Abel February 2016

You can do this a few different ways, but they wont involve the use of __getattr__.

What you really need to do is dynamically create your class, or at least dynamically create the wrapped functions on your class. There are a few ways to do this in python.

You could build the class definition using type() or metaclasses, or build it on class instantiation using the __new__ method.

Every time you call LoggingGithubRepositoryWrapper(), the __new__ method will be called. Here, it looks at all the attributes on the github_repository argument and finds all the non-private methods. It then creates a function on the instantiated LoggingGithubRepositoryWrapper class instance that wraps the repo call in a logging statement.

At the end, it passes back the modified class instance. Then __init__ is called.

from types import MethodType


class LoggingGithubRepositoryWrapper(object):

    def __new__(cls, github_repository):
        self = super(LoggingGithubRepositoryWrapper, cls).__new__(cls)
        for name in dir(github_repository):
            if name.startswith('__'):
                continue
            func = getattr(github_repository, name)            
            if isinstance(func, MethodType):                
                setattr(self, name, cls.log_wrap(func))
        return self

    @staticmethod
    def log_wrap(func):
        def wrap(*args, **kwargs):
            print 'Calling {0}'.format(func.__name__)
            return func(*args, **kwargs)
        return wrap

    def __init__(self, github_repository):
        ... # this is all the same

Post Status

Asked in February 2016
Viewed 2,836 times
Voted 9
Answered 1 times

Search




Leave an answer