Python dynamic help and autocomplete generation

柔情痞子 提交于 2021-02-08 03:39:26

问题


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?


回答1:


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.



来源:https://stackoverflow.com/questions/13603088/python-dynamic-help-and-autocomplete-generation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!