Python动态帮助和自动完成生成

4

我几乎拥有我想要的一切...

这是一个动态对象,封装了一个带有动态文档字符串生成的通用函数调用:

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

它的工作符合预期:

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

唯一的两个问题是帮助显示mutable_f签名。
>>> help(Dynamic().test)
Help on function mutable_f in module __main__:

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

现在的问题是没有自动补全功能(我可以实时获取有效函数列表并进行缓存,因为这个操作比较耗费时间)。

我认为第一个问题无法解决,但对于第二个问题我不太确定。有什么好的想法吗?


第一个问题也是可以解决的,方法是创建一个与签名匹配的门面。虽然不太美观,但是可行的。请参见我曾经构建的_buildFacade函数。您还可以在那里添加自己的文档字符串。 - Martijn Pieters
@MartijnPieters 我一定会去看看,谢谢! - estani
1个回答

12

自动完成通常利用dir()函数的输出结果,可以进行挂钩。只需实现一个__dir__()方法:

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

要在匹配函数签名的同时包装函数,您需要基于该签名构建一个外观。我已经为Zope安全功能做了完全相同的事情

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]

这将在方法签名方面执行正确的操作,几乎。你无法绕过可变默认值,在此需要明确处理它们以保留它们。

一个小演示:

>>> 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'}
整个_Default舞蹈需要保留可变默认值,尽管通常不是一个好主意,但确实需要像最初预期的那样继续工作。所构建的外观将看起来与原始外观一样,并且会像原始外观一样运行,但可变对象仍然存在于“正确”的位置。
请注意,外观会被更新为尽可能接近原始外观;通过使用functools.update_wrapper,从原始外观复制了各种元数据到外观中,但我们要小心排除__doc__字符串,因为我们的外观明确使用自己的docstring。

非常有趣...我花了一些时间才理解它。而且我仍然不确定如何在我的情况下使用它,但它给了我很多新的想法。也许我可以通过使用您的描述将它们添加到动态类中来“缓存”这些函数。感谢您的回答! - estani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接