diff options
| author | Sylvain Thénault <sylvain.thenault@logilab.fr> | 2010-05-11 12:23:19 +0200 | 
|---|---|---|
| committer | Sylvain Thénault <sylvain.thenault@logilab.fr> | 2010-05-11 12:23:19 +0200 | 
| commit | 0c10f49a9ff35264ff0131292c857cf5a07b8411 (patch) | |
| tree | d00e5369d0032c15c13bacd9fa8d1892d66a7d90 /gui.py | |
| parent | bd55aea1eabdff936e16f2a8210e9cd4fa0093ad (diff) | |
| download | pylint-git-0c10f49a9ff35264ff0131292c857cf5a07b8411.tar.gz | |
new pylint gui, contributed by Scott Pilkey and friends from the Tahiti team
Diffstat (limited to 'gui.py')
| -rw-r--r-- | gui.py | 419 | 
1 files changed, 385 insertions, 34 deletions
| @@ -1,48 +1,332 @@  """Tkinker gui for pylint""" -from Tkinter import Tk, Frame, Listbox, Entry, Label, Button, Scrollbar -from Tkinter import TOP, LEFT, RIGHT, BOTTOM, END, X, Y, BOTH  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 -if sys.platform.startswith('win'): -    PYLINT = 'pylint.bat' -else: -    PYLINT = 'pylint'  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('<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) + +        #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('<Return>', self.run_lint)          self.txtModule.pack(side=LEFT, expand=True, fill=X) -        Button(top_frame, text='Run', command=self.run_lint).pack(side=LEFT) -        scrl = Scrollbar(res_frame) +        #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, -                               background='white', -                               font='fixedsys', -                               selectmode='browse', -                               yscrollcommand=scrl.set) -        scrl.configure(command=self.results.yview) -        self.results.pack(side=LEFT, expand=True, fill=BOTH) -        scrl.pack(side=RIGHT, fill=Y) -         +                  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) -        #self.root.bind('<ctrl-q>', self.quit) + +        #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() @@ -51,25 +335,92 @@ class LintGui:          """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""" -        colors = {'W:':'red1', 'E:': 'red4', -                  'W:': 'red3', '**': 'navy'} -         +        self.update_filenames()          self.root.configure(cursor='watch') -        self.results.focus_set() -        self.results.delete(0, END) -        self.results.update() +        self.reporter = GUIReporter(self, output=self.report_stream)          module = self.txtModule.get() -        pout = os.popen('%s %s' % (PYLINT, module), 'r') -        for line in  pout.xreadlines(): -            line = line.rstrip() -            self.results.insert(END, line) -            fg_color = colors.get(line[:2], 'black') -            self.results.itemconfigure(END, fg=fg_color) -            self.results.update() +        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: @@ -79,4 +430,4 @@ def Run(args):      gui.mainloop()  if __name__ == '__main__': -    Run(sys.argv[1:])  +    Run(sys.argv[1:]) | 
