"""Tkinker gui for pylint""" import os import sys import re import Queue from threading import Thread from Tkinter import Tk, Frame, Listbox, Entry, Label, Button, Scrollbar, Checkbutton, Radiobutton, IntVar, StringVar from Tkinter import TOP, LEFT, RIGHT, BOTTOM, END, X, Y, BOTH, SUNKEN, W, HORIZONTAL, DISABLED, NORMAL, W, E from 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'} class BasicStream: ''' 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.nextTitle = 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.nextTitle) self.outdict[self.currout].pop() self.currout = self.nextTitle self.outdict[self.currout] = [''] if text.strip(): self.nextTitle = text.strip() if text.startswith('\n'): self.contents.append('') if self.currout: self.outdict[self.currout].append('') self.contents[-1] += text.strip('\n') if self.currout: self.outdict[self.currout][-1] += text.strip('\n') if text.endswith('\n') 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: numEmpty = self.outdict[item].count('') for i in range(numEmpty): 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: self.gui.rating.set('Error') self.gui.refresh_results_window() #reset stream variables for next run self.contents = [] self.outdict = {} self.currout = None self.nextTitle = None class LintGui: """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 = Queue.Queue() self.msgs = [] self.filenames = [] self.rating = StringVar() self.tabs = {} self.report_stream = BasicStream(self) #gui objects self.lbMessages = 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""" #setting up frames top_frame = Frame(self.root) mid_frame = Frame(self.root) radio_frame = Frame(self.root) res_frame = Frame(self.root) msg_frame = Frame(self.root) check_frame = Frame(self.root) history_frame = Frame(self.root) btn_frame = Frame(self.root) rating_frame = Frame(self.root) 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=BOTH, expand=True) rating_frame.pack(side=TOP, fill=BOTH, expand=True) res_frame.pack(side=TOP, fill=BOTH, expand=True) check_frame.pack(side=TOP, fill=BOTH, expand=True) msg_frame.pack(side=TOP, fill=BOTH, expand=True) btn_frame.pack(side=TOP, fill=X) #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.lbMessages = Listbox(msg_frame, yscrollcommand=rightscrollbar.set, xscrollcommand=bottomscrollbar.set, bg="white") self.lbMessages.pack(expand=True, fill=BOTH) rightscrollbar.config(command=self.lbMessages.yview) bottomscrollbar.config(command=self.lbMessages.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('', 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) #labels self.lblRatingLabel = Label(rating_frame, text='Rating:') self.lblRatingLabel.pack(side=LEFT) self.lblRating = Label(rating_frame, textvariable=self.rating) self.lblRating.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.txtModule = Entry(top_frame, background='white') self.txtModule.bind('', self.run_lint) self.txtModule.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) rawMet = 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) msgCat = 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) report.select() report.grid(column=0, row=0, sticky=W) rawMet.grid(column=1, row=0, sticky=W) dup.grid(column=2, row=0, sticky=W) msg.grid(column=3, row=0, sticky=E) stat.grid(column=0, row=1, sticky=W) msgCat.grid(column=1, row=1, sticky=W) ext.grid(column=2, row=1, columnspan=2, 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.txtModule.focus_set() def select_recent_file(self, event): """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.txtModule.delete(0, END) self.txtModule.insert(0, item) def refresh_msg_window(self): """refresh the message window with current output""" #clear the window self.lbMessages.delete(0, END) for msg in self.msgs: if (self.msg_type_dict.get(msg[0])()): msg_str = self.convert_to_string(msg) self.lbMessages.insert(END, msg_str) fg_color = COLORS.get(msg_str[:3], 'black') self.lbMessages.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: pass def convert_to_string(self, msg): """make a string representation of a message""" if (msg[2] != ""): return "(" + msg[0] + ") " + msg[1] + "." + msg[2] + " [" + msg[3] + "]: " + msg[4] else: return "(" + msg[0] + ") " + msg[1] + " [" + msg[3] + "]: " + msg[4] 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[0])()): msg_str = self.convert_to_string(msg) self.lbMessages.insert(END, msg_str) fg_color = COLORS.get(msg_str[:3], 'black') self.lbMessages.itemconfigure(END, fg=fg_color) except 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): """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.txtModule.delete(0, END) self.txtModule.insert(0, filename) def update_filenames(self): """update the list of recent filenames""" filename = self.txtModule.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.txtModule.get() if not module: module = os.getcwd() #cleaning up msgs and windows self.msgs = [] self.lbMessages.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 lint_thread(module, reporter, gui): """thread for pylint""" gui.status.text = "processing module(s)" lint_obj = 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' return gui = LintGui() gui.mainloop() if __name__ == '__main__': Run(sys.argv[1:])