内存文件/流中的pylint

时间:2013-10-23 12:56:41

标签: python editor static-analysis pylint

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

因此有两个问题:首先,如果不将输入的文本写入临时文件并将其提供给pylint,我该怎么做?我想在某些时候pylint(或astroid)处理流而不是文件。

更重要的是,这是一个好主意吗?它会导致进口或其他东西出现问题吗?直觉上我会说不,因为它似乎产生了一个新进程(带epylint)但我不是python专家所以我真的不确定。如果我使用this启动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: '<?>'

明天我会继续挖掘。 :)

2 个答案:

答案 0 :(得分:1)

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

Astroid支持从流构建AST,但这不是通过Pylint使用/暴露的,这是一个更高级别并且设计用于处理文件。因此,虽然您可以实现这一点,但还需要深入研究低级API。

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

答案 1 :(得分:1)

我跑了。

第一个(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)

可以在调用check_astroid_module之前调用monkeypatching。但还有一件事要做。看,有更隐含的行为:一些检查器期望并使用astroid的{​​{1}}字段。所以我们现在将这段代码放在file_encoding

的中间
check

可以说没有任何数量的linting会产生实际上很好的代码。不幸的是,pylint通过在文件上调用它的特殊性来实现极大的复杂性。非常好的代码有一个很好的原生API,并用CLI界面包装。不要问我为什么在内部存在file_stream,Module会从中构建,但会忘记源代码。

PS:我必须在你的代码中更改其他内容: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) 必须先出现在其他内容之前(也许是load_default_plugins,也许是......其他)

PPS:我建议继承BaseReporter并使用它代替你的prepare_checkers

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

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