Index: third_party/pylint/pylint/gui.py |
diff --git a/third_party/pylint/pylint/gui.py b/third_party/pylint/pylint/gui.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9c9b13893532ac41f754b7ba2db516868b35f365 |
--- /dev/null |
+++ b/third_party/pylint/pylint/gui.py |
@@ -0,0 +1,531 @@ |
+# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). |
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr |
+# |
+# This program is free software; you can redistribute it and/or modify it under |
+# the terms of the GNU General Public License as published by the Free Software |
+# Foundation; either version 2 of the License, or (at your option) any later |
+# version. |
+# |
+# This program is distributed in the hope that it will be useful, but WITHOUT |
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details |
+# |
+# You should have received a copy of the GNU General Public License along with |
+# this program; if not, write to the Free Software Foundation, Inc., |
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
+"""Tkinker gui for pylint""" |
+from __future__ import print_function |
+ |
+import os |
+import sys |
+import re |
+from threading import Thread |
+ |
+import six |
+ |
+from six.moves.tkinter import ( |
+ Tk, Frame, Listbox, Entry, Label, Button, Scrollbar, |
+ Checkbutton, Radiobutton, IntVar, StringVar, PanedWindow, |
+ TOP, LEFT, RIGHT, BOTTOM, END, X, Y, BOTH, SUNKEN, W, |
+ HORIZONTAL, DISABLED, NORMAL, W, |
+) |
+from six.moves.tkinter_tkfiledialog import ( |
+ askopenfilename, askdirectory, |
+) |
+ |
+import pylint.lint |
+from pylint.reporters.guireporter import GUIReporter |
+ |
+HOME = os.path.expanduser('~/') |
+HISTORY = '.pylint-gui-history' |
+COLORS = {'(I)':'lightblue', |
+ '(C)':'blue', '(R)':'darkblue', |
+ '(W)':'black', '(E)':'darkred', |
+ '(F)':'red'} |
+ |
+ |
+def convert_to_string(msg): |
+ """make a string representation of a message""" |
+ module_object = msg.module |
+ if msg.obj: |
+ module_object += ".%s" % msg.obj |
+ return "(%s) %s [%d]: %s" % (msg.C, module_object, msg.line, msg.msg) |
+ |
+class BasicStream(object): |
+ ''' |
+ used in gui reporter instead of writing to stdout, it is written to |
+ this stream and saved in contents |
+ ''' |
+ def __init__(self, gui): |
+ """init""" |
+ self.curline = "" |
+ self.gui = gui |
+ self.contents = [] |
+ self.outdict = {} |
+ self.currout = None |
+ self.next_title = None |
+ |
+ def write(self, text): |
+ """write text to the stream""" |
+ if re.match('^--+$', text.strip()) or re.match('^==+$', text.strip()): |
+ if self.currout: |
+ self.outdict[self.currout].remove(self.next_title) |
+ self.outdict[self.currout].pop() |
+ self.currout = self.next_title |
+ self.outdict[self.currout] = [''] |
+ |
+ if text.strip(): |
+ self.next_title = text.strip() |
+ |
+ if text.startswith(os.linesep): |
+ self.contents.append('') |
+ if self.currout: |
+ self.outdict[self.currout].append('') |
+ self.contents[-1] += text.strip(os.linesep) |
+ if self.currout: |
+ self.outdict[self.currout][-1] += text.strip(os.linesep) |
+ if text.endswith(os.linesep) and text.strip(): |
+ self.contents.append('') |
+ if self.currout: |
+ self.outdict[self.currout].append('') |
+ |
+ def fix_contents(self): |
+ """finalize what the contents of the dict should look like before output""" |
+ for item in self.outdict: |
+ num_empty = self.outdict[item].count('') |
+ for _ in range(num_empty): |
+ self.outdict[item].remove('') |
+ if self.outdict[item]: |
+ self.outdict[item].pop(0) |
+ |
+ def output_contents(self): |
+ """output contents of dict to the gui, and set the rating""" |
+ self.fix_contents() |
+ self.gui.tabs = self.outdict |
+ try: |
+ self.gui.rating.set(self.outdict['Global evaluation'][0]) |
+ except KeyError: |
+ self.gui.rating.set('Error') |
+ self.gui.refresh_results_window() |
+ |
+ #reset stream variables for next run |
+ self.contents = [] |
+ self.outdict = {} |
+ self.currout = None |
+ self.next_title = None |
+ |
+ |
+class LintGui(object): |
+ """Build and control a window to interact with pylint""" |
+ |
+ def __init__(self, root=None): |
+ """init""" |
+ self.root = root or Tk() |
+ self.root.title('Pylint') |
+ #reporter |
+ self.reporter = None |
+ #message queue for output from reporter |
+ self.msg_queue = six.moves.queue.Queue() |
+ self.msgs = [] |
+ self.visible_msgs = [] |
+ self.filenames = [] |
+ self.rating = StringVar() |
+ self.tabs = {} |
+ self.report_stream = BasicStream(self) |
+ #gui objects |
+ self.lb_messages = None |
+ self.showhistory = None |
+ self.results = None |
+ self.btnRun = None |
+ self.information_box = None |
+ self.convention_box = None |
+ self.refactor_box = None |
+ self.warning_box = None |
+ self.error_box = None |
+ self.fatal_box = None |
+ self.txtModule = None |
+ self.status = None |
+ self.msg_type_dict = None |
+ self.init_gui() |
+ |
+ def init_gui(self): |
+ """init helper""" |
+ |
+ window = PanedWindow(self.root, orient="vertical") |
+ window.pack(side=TOP, fill=BOTH, expand=True) |
+ |
+ top_pane = Frame(window) |
+ window.add(top_pane) |
+ mid_pane = Frame(window) |
+ window.add(mid_pane) |
+ bottom_pane = Frame(window) |
+ window.add(bottom_pane) |
+ |
+ #setting up frames |
+ top_frame = Frame(top_pane) |
+ mid_frame = Frame(top_pane) |
+ history_frame = Frame(top_pane) |
+ radio_frame = Frame(mid_pane) |
+ rating_frame = Frame(mid_pane) |
+ res_frame = Frame(mid_pane) |
+ check_frame = Frame(bottom_pane) |
+ msg_frame = Frame(bottom_pane) |
+ btn_frame = Frame(bottom_pane) |
+ top_frame.pack(side=TOP, fill=X) |
+ mid_frame.pack(side=TOP, fill=X) |
+ history_frame.pack(side=TOP, fill=BOTH, expand=True) |
+ radio_frame.pack(side=TOP, fill=X) |
+ rating_frame.pack(side=TOP, fill=X) |
+ res_frame.pack(side=TOP, fill=BOTH, expand=True) |
+ check_frame.pack(side=TOP, fill=X) |
+ msg_frame.pack(side=TOP, fill=BOTH, expand=True) |
+ btn_frame.pack(side=TOP, fill=X) |
+ |
+ # Binding F5 application-wide to run lint |
+ self.root.bind('<F5>', self.run_lint) |
+ |
+ #Message ListBox |
+ rightscrollbar = Scrollbar(msg_frame) |
+ rightscrollbar.pack(side=RIGHT, fill=Y) |
+ bottomscrollbar = Scrollbar(msg_frame, orient=HORIZONTAL) |
+ bottomscrollbar.pack(side=BOTTOM, fill=X) |
+ self.lb_messages = Listbox( |
+ msg_frame, |
+ yscrollcommand=rightscrollbar.set, |
+ xscrollcommand=bottomscrollbar.set, |
+ bg="white") |
+ self.lb_messages.bind("<Double-Button-1>", self.show_sourcefile) |
+ self.lb_messages.pack(expand=True, fill=BOTH) |
+ rightscrollbar.config(command=self.lb_messages.yview) |
+ bottomscrollbar.config(command=self.lb_messages.xview) |
+ |
+ #History ListBoxes |
+ rightscrollbar2 = Scrollbar(history_frame) |
+ rightscrollbar2.pack(side=RIGHT, fill=Y) |
+ bottomscrollbar2 = Scrollbar(history_frame, orient=HORIZONTAL) |
+ bottomscrollbar2.pack(side=BOTTOM, fill=X) |
+ self.showhistory = Listbox( |
+ history_frame, |
+ yscrollcommand=rightscrollbar2.set, |
+ xscrollcommand=bottomscrollbar2.set, |
+ bg="white") |
+ self.showhistory.pack(expand=True, fill=BOTH) |
+ rightscrollbar2.config(command=self.showhistory.yview) |
+ bottomscrollbar2.config(command=self.showhistory.xview) |
+ self.showhistory.bind('<Double-Button-1>', self.select_recent_file) |
+ self.set_history_window() |
+ |
+ #status bar |
+ self.status = Label(self.root, text="", bd=1, relief=SUNKEN, anchor=W) |
+ self.status.pack(side=BOTTOM, fill=X) |
+ |
+ #labelbl_ratingls |
+ lbl_rating_label = Label(rating_frame, text='Rating:') |
+ lbl_rating_label.pack(side=LEFT) |
+ lbl_rating = Label(rating_frame, textvariable=self.rating) |
+ lbl_rating.pack(side=LEFT) |
+ Label(mid_frame, text='Recently Used:').pack(side=LEFT) |
+ Label(top_frame, text='Module or package').pack(side=LEFT) |
+ |
+ #file textbox |
+ self.txt_module = Entry(top_frame, background='white') |
+ self.txt_module.bind('<Return>', self.run_lint) |
+ self.txt_module.pack(side=LEFT, expand=True, fill=X) |
+ |
+ #results box |
+ rightscrollbar = Scrollbar(res_frame) |
+ rightscrollbar.pack(side=RIGHT, fill=Y) |
+ bottomscrollbar = Scrollbar(res_frame, orient=HORIZONTAL) |
+ bottomscrollbar.pack(side=BOTTOM, fill=X) |
+ self.results = Listbox( |
+ res_frame, |
+ yscrollcommand=rightscrollbar.set, |
+ xscrollcommand=bottomscrollbar.set, |
+ bg="white", font="Courier") |
+ self.results.pack(expand=True, fill=BOTH, side=BOTTOM) |
+ rightscrollbar.config(command=self.results.yview) |
+ bottomscrollbar.config(command=self.results.xview) |
+ |
+ #buttons |
+ Button(top_frame, text='Open', command=self.file_open).pack(side=LEFT) |
+ Button(top_frame, text='Open Package', |
+ command=(lambda: self.file_open(package=True))).pack(side=LEFT) |
+ |
+ self.btnRun = Button(top_frame, text='Run', command=self.run_lint) |
+ self.btnRun.pack(side=LEFT) |
+ Button(btn_frame, text='Quit', command=self.quit).pack(side=BOTTOM) |
+ |
+ #radio buttons |
+ self.information_box = IntVar() |
+ self.convention_box = IntVar() |
+ self.refactor_box = IntVar() |
+ self.warning_box = IntVar() |
+ self.error_box = IntVar() |
+ self.fatal_box = IntVar() |
+ i = Checkbutton(check_frame, text="Information", fg=COLORS['(I)'], |
+ variable=self.information_box, command=self.refresh_msg_window) |
+ c = Checkbutton(check_frame, text="Convention", fg=COLORS['(C)'], |
+ variable=self.convention_box, command=self.refresh_msg_window) |
+ r = Checkbutton(check_frame, text="Refactor", fg=COLORS['(R)'], |
+ variable=self.refactor_box, command=self.refresh_msg_window) |
+ w = Checkbutton(check_frame, text="Warning", fg=COLORS['(W)'], |
+ variable=self.warning_box, command=self.refresh_msg_window) |
+ e = Checkbutton(check_frame, text="Error", fg=COLORS['(E)'], |
+ variable=self.error_box, command=self.refresh_msg_window) |
+ f = Checkbutton(check_frame, text="Fatal", fg=COLORS['(F)'], |
+ variable=self.fatal_box, command=self.refresh_msg_window) |
+ i.select() |
+ c.select() |
+ r.select() |
+ w.select() |
+ e.select() |
+ f.select() |
+ i.pack(side=LEFT) |
+ c.pack(side=LEFT) |
+ r.pack(side=LEFT) |
+ w.pack(side=LEFT) |
+ e.pack(side=LEFT) |
+ f.pack(side=LEFT) |
+ |
+ #check boxes |
+ self.box = StringVar() |
+ # XXX should be generated |
+ report = Radiobutton( |
+ radio_frame, text="Report", variable=self.box, |
+ value="Report", command=self.refresh_results_window) |
+ raw_met = Radiobutton( |
+ radio_frame, text="Raw metrics", variable=self.box, |
+ value="Raw metrics", command=self.refresh_results_window) |
+ dup = Radiobutton( |
+ radio_frame, text="Duplication", variable=self.box, |
+ value="Duplication", command=self.refresh_results_window) |
+ ext = Radiobutton( |
+ radio_frame, text="External dependencies", |
+ variable=self.box, value="External dependencies", |
+ command=self.refresh_results_window) |
+ stat = Radiobutton( |
+ radio_frame, text="Statistics by type", |
+ variable=self.box, value="Statistics by type", |
+ command=self.refresh_results_window) |
+ msg_cat = Radiobutton( |
+ radio_frame, text="Messages by category", |
+ variable=self.box, value="Messages by category", |
+ command=self.refresh_results_window) |
+ msg = Radiobutton( |
+ radio_frame, text="Messages", variable=self.box, |
+ value="Messages", command=self.refresh_results_window) |
+ source_file = Radiobutton( |
+ radio_frame, text="Source File", variable=self.box, |
+ value="Source File", command=self.refresh_results_window) |
+ report.select() |
+ report.grid(column=0, row=0, sticky=W) |
+ raw_met.grid(column=1, row=0, sticky=W) |
+ dup.grid(column=2, row=0, sticky=W) |
+ msg.grid(column=3, row=0, sticky=W) |
+ stat.grid(column=0, row=1, sticky=W) |
+ msg_cat.grid(column=1, row=1, sticky=W) |
+ ext.grid(column=2, row=1, sticky=W) |
+ source_file.grid(column=3, row=1, sticky=W) |
+ |
+ #dictionary for check boxes and associated error term |
+ self.msg_type_dict = { |
+ 'I': lambda: self.information_box.get() == 1, |
+ 'C': lambda: self.convention_box.get() == 1, |
+ 'R': lambda: self.refactor_box.get() == 1, |
+ 'E': lambda: self.error_box.get() == 1, |
+ 'W': lambda: self.warning_box.get() == 1, |
+ 'F': lambda: self.fatal_box.get() == 1 |
+ } |
+ self.txt_module.focus_set() |
+ |
+ |
+ def select_recent_file(self, event): # pylint: disable=unused-argument |
+ """adds the selected file in the history listbox to the Module box""" |
+ if not self.showhistory.size(): |
+ return |
+ |
+ selected = self.showhistory.curselection() |
+ item = self.showhistory.get(selected) |
+ #update module |
+ self.txt_module.delete(0, END) |
+ self.txt_module.insert(0, item) |
+ |
+ def refresh_msg_window(self): |
+ """refresh the message window with current output""" |
+ #clear the window |
+ self.lb_messages.delete(0, END) |
+ self.visible_msgs = [] |
+ for msg in self.msgs: |
+ if self.msg_type_dict.get(msg.C)(): |
+ self.visible_msgs.append(msg) |
+ msg_str = convert_to_string(msg) |
+ self.lb_messages.insert(END, msg_str) |
+ fg_color = COLORS.get(msg_str[:3], 'black') |
+ self.lb_messages.itemconfigure(END, fg=fg_color) |
+ |
+ def refresh_results_window(self): |
+ """refresh the results window with current output""" |
+ #clear the window |
+ self.results.delete(0, END) |
+ try: |
+ for res in self.tabs[self.box.get()]: |
+ self.results.insert(END, res) |
+ except KeyError: |
+ pass |
+ |
+ def process_incoming(self): |
+ """process the incoming messages from running pylint""" |
+ while self.msg_queue.qsize(): |
+ try: |
+ msg = self.msg_queue.get(0) |
+ if msg == "DONE": |
+ self.report_stream.output_contents() |
+ return False |
+ |
+ #adding message to list of msgs |
+ self.msgs.append(msg) |
+ |
+ #displaying msg if message type is selected in check box |
+ if self.msg_type_dict.get(msg.C)(): |
+ self.visible_msgs.append(msg) |
+ msg_str = convert_to_string(msg) |
+ self.lb_messages.insert(END, msg_str) |
+ fg_color = COLORS.get(msg_str[:3], 'black') |
+ self.lb_messages.itemconfigure(END, fg=fg_color) |
+ |
+ except six.moves.queue.Empty: |
+ pass |
+ return True |
+ |
+ def periodic_call(self): |
+ """determine when to unlock the run button""" |
+ if self.process_incoming(): |
+ self.root.after(100, self.periodic_call) |
+ else: |
+ #enabling button so it can be run again |
+ self.btnRun.config(state=NORMAL) |
+ |
+ def mainloop(self): |
+ """launch the mainloop of the application""" |
+ self.root.mainloop() |
+ |
+ def quit(self, _=None): |
+ """quit the application""" |
+ self.root.quit() |
+ |
+ def halt(self): # pylint: disable=no-self-use |
+ """program halt placeholder""" |
+ return |
+ |
+ def file_open(self, package=False, _=None): |
+ """launch a file browser""" |
+ if not package: |
+ filename = askopenfilename(parent=self.root, |
+ filetypes=[('pythonfiles', '*.py'), |
+ ('allfiles', '*')], |
+ title='Select Module') |
+ else: |
+ filename = askdirectory(title="Select A Folder", mustexist=1) |
+ |
+ if filename == (): |
+ return |
+ |
+ self.txt_module.delete(0, END) |
+ self.txt_module.insert(0, filename) |
+ |
+ def update_filenames(self): |
+ """update the list of recent filenames""" |
+ filename = self.txt_module.get() |
+ if not filename: |
+ filename = os.getcwd() |
+ if filename+'\n' in self.filenames: |
+ index = self.filenames.index(filename+'\n') |
+ self.filenames.pop(index) |
+ |
+ #ensure only 10 most recent are stored |
+ if len(self.filenames) == 10: |
+ self.filenames.pop() |
+ self.filenames.insert(0, filename+'\n') |
+ |
+ def set_history_window(self): |
+ """update the history window with info from the history file""" |
+ #clear the window |
+ self.showhistory.delete(0, END) |
+ # keep the last 10 most recent files |
+ try: |
+ view_history = open(HOME+HISTORY, 'r') |
+ for hist in view_history.readlines(): |
+ if not hist in self.filenames: |
+ self.filenames.append(hist) |
+ self.showhistory.insert(END, hist.split('\n')[0]) |
+ view_history.close() |
+ except IOError: |
+ # do nothing since history file will be created later |
+ return |
+ |
+ def run_lint(self, _=None): |
+ """launches pylint""" |
+ self.update_filenames() |
+ self.root.configure(cursor='watch') |
+ self.reporter = GUIReporter(self, output=self.report_stream) |
+ module = self.txt_module.get() |
+ if not module: |
+ module = os.getcwd() |
+ |
+ #cleaning up msgs and windows |
+ self.msgs = [] |
+ self.visible_msgs = [] |
+ self.lb_messages.delete(0, END) |
+ self.tabs = {} |
+ self.results.delete(0, END) |
+ self.btnRun.config(state=DISABLED) |
+ |
+ #setting up a worker thread to run pylint |
+ worker = Thread(target=lint_thread, args=(module, self.reporter, self,)) |
+ self.periodic_call() |
+ worker.start() |
+ |
+ # Overwrite the .pylint-gui-history file with all the new recently added files |
+ # in order from filenames but only save last 10 files |
+ write_history = open(HOME+HISTORY, 'w') |
+ write_history.writelines(self.filenames) |
+ write_history.close() |
+ self.set_history_window() |
+ |
+ self.root.configure(cursor='') |
+ |
+ def show_sourcefile(self, event=None): # pylint: disable=unused-argument |
+ selected = self.lb_messages.curselection() |
+ if not selected: |
+ return |
+ |
+ msg = self.visible_msgs[int(selected[0])] |
+ scroll = msg.line - 3 |
+ if scroll < 0: |
+ scroll = 0 |
+ |
+ self.tabs["Source File"] = open(msg.path, "r").readlines() |
+ self.box.set("Source File") |
+ self.refresh_results_window() |
+ self.results.yview(scroll) |
+ self.results.select_set(msg.line - 1) |
+ |
+ |
+def lint_thread(module, reporter, gui): |
+ """thread for pylint""" |
+ gui.status.text = "processing module(s)" |
+ pylint.lint.Run(args=[module], reporter=reporter, exit=False) |
+ gui.msg_queue.put("DONE") |
+ |
+ |
+def Run(args): |
+ """launch pylint gui from args""" |
+ if args: |
+ print('USAGE: pylint-gui\n launch a simple pylint gui using Tk') |
+ sys.exit(1) |
+ gui = LintGui() |
+ gui.mainloop() |
+ sys.exit(0) |
+ |
+if __name__ == '__main__': |
+ Run(sys.argv[1:]) |