2013-10-23 65 views
2

我想在程序中嵌入pylint。用戶輸入python程序(在Qt中,在QTextEdit中,雖然不相關),在後臺我調用pylint來檢查他輸入的文本。最後,我在消息框中打印錯誤。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: '<?>' 

我將繼續挖掘的明天。 :)

回答

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) 

那可以的monkeypatching只是打電話check_astroid_module之前被調用。但還有一件事要做。看,有更多的隱性行爲:一些跳棋期望和使用astroidfile_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) 

可以說,不掉毛的量實際上創造良好的代碼。不幸的是,pylint將專業化的文件稱爲文件,使得它非常複雜。真正好的代碼有一個很好的本地API,並用CLI界面包裝它。不要問我爲什麼file_stream在內部存在,模塊從內建,但忘記了源代碼。

PS:

PPS load_default_plugins有來過一些其他的東西:我不得不改變別人......在你的代碼(否則可能prepare_checkers,也許某事):我建議繼承BaseReporter和使用,而不是你InMemoryMessagesBuffer

PPPS:這個剛剛拉(3.2014),並會解決這個問題:https://bitbucket.org/logilab/astroid/pull-request/15/astroidbuilderstring_build-was/diff

4PS:這是目前在正式版,所以沒有猴子補丁需要:astroid.scoped_nodes.Module現在有一個file_bytes財產(不帶前導聯合國derscore)。

+0

這太棒了。我沒有測試你的建議。您是否設法在沒有請求的情況下使用它,還是強制性的?如果您可以發佈您所需或建議的變更的要點,我將不勝感激。 (說實話,這個請求是50%的懶惰,50%的困惑):) – ibizaman

+1

monkeypatch與'file_encoding'和'_file_bytes'字段的顯式設置一致,取代了拉請求,是的。並且一旦拉取請求被拉出,它就不會破壞代碼。所以現在就使用它,並且在請求被拉出並且一個新的astroid版本出來後,或者刪除'astroid不是None'之後的三行或者忘記這麼做:)並且如下所示:添加'if astroid不是None「修復* your *代碼中的錯誤。 –

1

使用無法定位的流可能會導致相對導入時出現問題,因爲該位置因此需要查找實際導入的模塊。

Astroid支持從流中構建AST,但這不會通過Pylint使用/暴露,該Pylint級別更高,且設計用於處理文件。所以,儘管你可能會覺得這樣,但它需要對低級API進行一些挖掘。

最簡單的方法是確保將緩衝區保存到文件中,然後使用SA回答以編程方式啓動pylint(如果您願意)(完全忘記了在其他響應中發現的其他帳戶)。另一種選擇是寫一個自定義記者來獲得更多的控制權。

+0

所以,如果我遠離相對進口一切都會好嗎?如果是的話,潛入astroid的低級API似乎很容易。至少我會先訴諸臨時文件。 – ibizaman

+0

@ibizaman:有什麼進展?我想做的完全一樣,憎惡termpfiles。 –