5

我想将 pylint 嵌入到程序中。用户输入 python 程序(在 Qt 中,在 QTextEdit 中,虽然不相关),在后台我调用 pylint 来检查他输入的文本。最后,我在消息框中打印错误。

因此有两个问题:首先,如何在不将输入的文本写入临时文件并将其提供给 pylint 的情况下做到这一点?我想在某些时候 pylint (或 astroid )处理流而不是文件了。

而且,更重要的是,这是个好主意吗?它会对进口或其他东西造成问题吗?直觉上我会说不,因为它似乎产生了一个新进程(使用epylint),但我不是python专家,所以我真的不确定。如果我用来启动 pylint,也可以吗?

编辑:我尝试修补 pylint 的内部结构,事件与之斗争,但最终被困在某个点上。

这是到目前为止的代码:

from astroid.builder import AstroidBuilder
from astroid.exceptions import AstroidBuildingException
from logilab.common.interface import implements
from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker
from pylint.lint import PyLinter
from pylint.reporters.text import TextReporter
from pylint.utils import PyLintASTWalker

class Validator():
    def __init__(self):
        self._messagesBuffer = InMemoryMessagesBuffer()
        self._validator = None
        self.initValidator()

    def initValidator(self):
        self._validator = StringPyLinter(reporter=TextReporter(output=self._messagesBuffer))
        self._validator.load_default_plugins()
        self._validator.disable('W0704')
        self._validator.disable('I0020')
        self._validator.disable('I0021')
        self._validator.prepare_import_path([])

    def destroyValidator(self):
        self._validator.cleanup_import_path()

    def check(self, string):
        return self._validator.check(string)


class InMemoryMessagesBuffer():
    def __init__(self):
        self.content = []
    def write(self, st):
        self.content.append(st)
    def messages(self):
        return self.content
    def reset(self):
        self.content = []

class StringPyLinter(PyLinter):
    """Does what PyLinter does but sets checkers once
    and redefines get_astroid to call build_string"""
    def __init__(self, options=(), reporter=None, option_groups=(), pylintrc=None):
        super(StringPyLinter, self).__init__(options, reporter, option_groups, pylintrc)
        self._walker = None
        self._used_checkers = None
        self._tokencheckers = None
        self._rawcheckers = None
        self.initCheckers()

    def __del__(self):
        self.destroyCheckers()

    def initCheckers(self):
        self._walker = PyLintASTWalker(self)
        self._used_checkers = self.prepare_checkers()
        self._tokencheckers = [c for c in self._used_checkers if implements(c, ITokenChecker)
                               and c is not self]
        self._rawcheckers = [c for c in self._used_checkers if implements(c, IRawChecker)]
        # notify global begin
        for checker in self._used_checkers:
            checker.open()
            if implements(checker, IAstroidChecker):
                self._walker.add_checker(checker)

    def destroyCheckers(self):
        self._used_checkers.reverse()
        for checker in self._used_checkers:
            checker.close()

    def check(self, string):
        modname = "in_memory"
        self.set_current_module(modname)

        astroid = self.get_astroid(string, modname)
        self.check_astroid_module(astroid, self._walker, self._rawcheckers, self._tokencheckers)

        self._add_suppression_messages()
        self.set_current_module('')
        self.stats['statement'] = self._walker.nbstatements

    def get_astroid(self, string, modname):
        """return an astroid representation for a module"""
        try:
            return AstroidBuilder().string_build(string, modname)
        except SyntaxError as ex:
            self.add_message('E0001', line=ex.lineno, args=ex.msg)
        except AstroidBuildingException as ex:
            self.add_message('F0010', args=ex)
        except Exception as ex:
            import traceback
            traceback.print_exc()
            self.add_message('F0002', args=(ex.__class__, ex))


if __name__ == '__main__':
    code = """
    a = 1
    print(a)
    """

    validator = Validator()
    print(validator.check(code))

回溯如下:

Traceback (most recent call last):
  File "validator.py", line 16, in <module>
    main()
  File "validator.py", line 13, in main
    print(validator.check(code))
  File "validator.py", line 30, in check
    self._validator.check(string)
  File "validator.py", line 79, in check
    self.check_astroid_module(astroid, self._walker, self._rawcheckers, self._tokencheckers)
  File "c:\Python33\lib\site-packages\pylint\lint.py", line 659, in check_astroid_module
    tokens = tokenize_module(astroid)
  File "c:\Python33\lib\site-packages\pylint\utils.py", line 103, in tokenize_module
    print(module.file_stream)
AttributeError: 'NoneType' object has no attribute 'file_stream'
# And sometimes this is added :
  File "c:\Python33\lib\site-packages\astroid\scoped_nodes.py", line 251, in file_stream
    return open(self.file, 'rb')
OSError: [Errno 22] Invalid argument: '<?>'

明天继续挖。:)

4

2 回答 2

2

我让它运行起来。

第一个(NoneType ...)非常简单,并且在您的代码中存在错误:

遇到异常会导致get_astroid“失败”,即发送一条语法错误消息并返回!

但是对于第二个……pylint/logilab 的 API 中的这种废话……让我解释一下:你的astroid对象是astroid.scoped_nodes.Module.

它也是由工厂创建的AstroidBuilder,它设置astroid.file = '<?>'.

不幸的是,Module该类具有以下属性:

@property
def file_stream(self):
    if self.file is not None:
        return open(self.file, 'rb')
    return None

除了子类化(这将使我们无法在 中使用魔法AstroidBuilder)之外,没有其他方法可以跳过它,所以……猴子补丁!

我们将定义不明确的属性替换为astroid._file_bytes在执行上述默认行为之前检查实例是否引用我们的代码字节(例如 )的属性。

def _monkeypatch_module(module_class):
    if module_class.file_stream.fget.__name__ == 'file_stream_patched':
        return  # only patch if patch isn’t already applied

    old_file_stream_fget = module_class.file_stream.fget
    def file_stream_patched(self):
        if hasattr(self, '_file_bytes'):
            return BytesIO(self._file_bytes)
        return old_file_stream_fget(self)

    module_class.file_stream = property(file_stream_patched)

可以在调用之前调用monkeypatching check_astroid_module。但是还有一件事要做。看,还有更多隐含的行为:一些检查器期望并使用astroid'file_encoding字段。所以我们现在在中间有这段代码check

astroid = self.get_astroid(string, modname)
if astroid is not None:
    _monkeypatch_module(astroid.__class__)
    astroid._file_bytes = string.encode('utf-8')
    astroid.file_encoding = 'utf-8'

    self.check_astroid_module(astroid, self._walker, self._rawcheckers, self._tokencheckers)

可以说,再多的 linting 也不会产生真正好的代码。不幸的是,pylint 将巨大的复杂性与在文件上调用它的专业化结合在一起。真正好的代码有一个很好的原生 API 并用 CLI 接口包装它。不要问我为什么 file_stream 在内部存在,Module 是从内部构建的,但忘记了源代码。

PS:我不得不在你的代码中改变其他东西:load_default_plugins必须在其他东西之前(也许prepare_checkers,也许……其他)

PPS:我建议继承 BaseReporter 并使用它而不是你的InMemoryMessagesBuffer

PPPS:这刚刚被取消(3.2014),并将解决此问题:https ://bitbucket.org/logilab/astroid/pull-request/15/astroidbuilderstring_build-was/diff

4PS:这是现在正式版,所以不需要猴子补丁:astroid.scoped_nodes.Module现在有一个file_bytes属性(没有前导下划线)。

于 2013-11-23T22:13:47.490 回答
1

在相对导入的情况下,使用不可定位的流肯定会导致问题,因为需要该位置才能找到实际导入的模块。

Astroid 支持从流中构建 AST,但这不是通过 Pylint 使用/公开的,Pylint 级别更高,旨在处理文件。因此,虽然您可能会实现这一点,但需要对低级 API 进行一些研究。

最简单的方法是将缓冲区保存到文件中,然后如果您愿意,可以使用 SA 答案以编程方式启动 pylint(完全忘记了在其他响应中找到的我的其他帐户;)。另一种选择是编写自定义记者以获得更多控制权。

于 2013-10-23T14:12:59.160 回答