Search code examples
pythondynamicautocompletedocstring

Python dynamic help and autocomplete generation


I have almost what I want...

This dynamic object encapsulating a generic function call with a dynamic docstring generation:

def add_docs(tool):  
    def desc(func):  
        func.__doc__ = "Showing help for %s()" % tool
        return func
    return desc

class Dynamic(object):
    def __getattr__(self, value):
        @add_docs(value)
        def mutable_f(*args, **kwargs):
            print "Calling:", value
            print "With arguments:", args, kwargs

        return mutable_f

And it works as expected:

>>> Dynamic().test(1, input='file')
Calling: test
With arguments: (1,) {'input': 'file'}
>>> Dynamic().test.__doc__
'Showing help for test()'

The only two problems are that the help show the mutable_f signature

>>> help(Dynamic().test)
Help on function mutable_f in module __main__:

mutable_f(*args, **kwargs)
    Showing help for test()
(END)

And that there's no auto-completion (I can get a list of valid functions on-the-fly, and cache it because that operation is expensive)

I think the first one is unsolvable, but I'm not so sure about the second one. Ideas?


Solution

  • Autocompletion most often makes use of the output of the dir() function, which can be hooked. Simply implement a __dir__() method:

    def __dir__(self):
        res = dir(type(self)) + list(self.__dict__.keys())
        res.extend(['dynamic1', 'dynamic2'])
        return res
    

    As for wrapping a function while matching it's signature, you'll need to build a facade based on that signature. I've done exactly that for a Zope security feature:

    import inspect
    import functools
    
    
    class _Default(object):
        def __init__(self, repr):
            self._repr = repr
        def __repr__(self):
            return self._repr
    
    
    def _buildFacade(name, spec, docstring):
        """Build a facade function, matching the decorated method in signature.
    
        Note that defaults are replaced by instances of _Default, and _curried
        will reconstruct these to preserve mutable defaults.
    
        """
        args = inspect.formatargspec(
            formatvalue=lambda v: '=_Default({0!r})'.format(repr(v)), *spec)
        callargs = inspect.formatargspec(formatvalue=lambda v: '', *spec)
        return 'def {0}{1}:\n    """{2}"""\n    return _curried{3}'.format(
            name, args, docstring, callargs)
    
    
    def add_docs(tool):
        spec = inspect.getargspec(tool)
        args, defaults = spec[0], spec[3]
    
        arglen = len(args)
        if defaults is not None:
            defaults = zip(args[arglen - len(defaults):], defaults)
            arglen -= len(defaults)
    
        def _curried(*args, **kw):
            # Reconstruct keyword arguments
            if defaults is not None:
                args, kwparams = args[:arglen], args[arglen:]
                for positional, (key, default) in zip(kwparams, defaults):
                    if isinstance(positional, _Default):
                        kw[key] = default
                    else:
                        kw[key] = positional
    
            return tool(*args, **kw)
    
        name = tool.__name__
        doc = 'Showing help for {0}()'.format(name)
        facade_globs = dict(_curried=_curried, _Default=_Default)
        exec _buildFacade(name, spec, doc) in facade_globs
    
        wrapped = facade_globs[name]
        wrapped = functools.update_wrapper(wrapped, tool,
            assigned=filter(lambda w: w != '__doc__', functools.WRAPPER_ASSIGNMENTS))
    
        return facade_globs[name]
    

    This will do the correct thing when it comes to method signatures, almost. You cannot get around the mutable defaults here, and need to handle those explicitly to preserve them.

    A small demonstration:

    >>> def foo(bar, spam='eggs', foobarred={}):
    ...     foobarred[bar] = spam
    ...     print foobarred
    ... 
    >>> documented = add_docs(foo)
    >>> help(documented)
    Help on function foo:
    
    foo(bar, spam='eggs', foobarred={})
        Showing help for foo()
    
    >>> documented('monty', 'python')
    {'monty': 'python'}
    >>> documented('Eric', 'Idle')
    {'Eric': 'Idle', 'monty': 'python'}
    

    The whole _Default dance is required to preserve mutable defaults, which, although a generally a bad idea, do need to continue to work as originally intended. The facade built will look just like the original, and will act like it, but mutables continue to live in the 'correct' location.

    Note that the facade gets updated to match the original as closely as possible; by using functools.update_wrapper various pieces of metadata are copied over from the original to the facade, but we take care to exclude the __doc__ string from that, since our facade explicitly uses it's own docstring instead.