OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). |
| 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 3 # |
| 4 # This program is free software; you can redistribute it and/or modify it under |
| 5 # the terms of the GNU General Public License as published by the Free Software |
| 6 # Foundation; either version 2 of the License, or (at your option) any later |
| 7 # version. |
| 8 # |
| 9 # This program is distributed in the hope that it will be useful, but WITHOUT |
| 10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details |
| 12 # |
| 13 # You should have received a copy of the GNU General Public License along with |
| 14 # this program; if not, write to the Free Software Foundation, Inc., |
| 15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 16 """Tkinker gui for pylint""" |
| 17 from __future__ import print_function |
| 18 |
| 19 import os |
| 20 import sys |
| 21 import re |
| 22 from threading import Thread |
| 23 |
| 24 import six |
| 25 |
| 26 from six.moves.tkinter import ( |
| 27 Tk, Frame, Listbox, Entry, Label, Button, Scrollbar, |
| 28 Checkbutton, Radiobutton, IntVar, StringVar, PanedWindow, |
| 29 TOP, LEFT, RIGHT, BOTTOM, END, X, Y, BOTH, SUNKEN, W, |
| 30 HORIZONTAL, DISABLED, NORMAL, W, |
| 31 ) |
| 32 from six.moves.tkinter_tkfiledialog import ( |
| 33 askopenfilename, askdirectory, |
| 34 ) |
| 35 |
| 36 import pylint.lint |
| 37 from pylint.reporters.guireporter import GUIReporter |
| 38 |
| 39 HOME = os.path.expanduser('~/') |
| 40 HISTORY = '.pylint-gui-history' |
| 41 COLORS = {'(I)':'lightblue', |
| 42 '(C)':'blue', '(R)':'darkblue', |
| 43 '(W)':'black', '(E)':'darkred', |
| 44 '(F)':'red'} |
| 45 |
| 46 |
| 47 def convert_to_string(msg): |
| 48 """make a string representation of a message""" |
| 49 module_object = msg.module |
| 50 if msg.obj: |
| 51 module_object += ".%s" % msg.obj |
| 52 return "(%s) %s [%d]: %s" % (msg.C, module_object, msg.line, msg.msg) |
| 53 |
| 54 class BasicStream(object): |
| 55 ''' |
| 56 used in gui reporter instead of writing to stdout, it is written to |
| 57 this stream and saved in contents |
| 58 ''' |
| 59 def __init__(self, gui): |
| 60 """init""" |
| 61 self.curline = "" |
| 62 self.gui = gui |
| 63 self.contents = [] |
| 64 self.outdict = {} |
| 65 self.currout = None |
| 66 self.next_title = None |
| 67 |
| 68 def write(self, text): |
| 69 """write text to the stream""" |
| 70 if re.match('^--+$', text.strip()) or re.match('^==+$', text.strip()): |
| 71 if self.currout: |
| 72 self.outdict[self.currout].remove(self.next_title) |
| 73 self.outdict[self.currout].pop() |
| 74 self.currout = self.next_title |
| 75 self.outdict[self.currout] = [''] |
| 76 |
| 77 if text.strip(): |
| 78 self.next_title = text.strip() |
| 79 |
| 80 if text.startswith(os.linesep): |
| 81 self.contents.append('') |
| 82 if self.currout: |
| 83 self.outdict[self.currout].append('') |
| 84 self.contents[-1] += text.strip(os.linesep) |
| 85 if self.currout: |
| 86 self.outdict[self.currout][-1] += text.strip(os.linesep) |
| 87 if text.endswith(os.linesep) and text.strip(): |
| 88 self.contents.append('') |
| 89 if self.currout: |
| 90 self.outdict[self.currout].append('') |
| 91 |
| 92 def fix_contents(self): |
| 93 """finalize what the contents of the dict should look like before output
""" |
| 94 for item in self.outdict: |
| 95 num_empty = self.outdict[item].count('') |
| 96 for _ in range(num_empty): |
| 97 self.outdict[item].remove('') |
| 98 if self.outdict[item]: |
| 99 self.outdict[item].pop(0) |
| 100 |
| 101 def output_contents(self): |
| 102 """output contents of dict to the gui, and set the rating""" |
| 103 self.fix_contents() |
| 104 self.gui.tabs = self.outdict |
| 105 try: |
| 106 self.gui.rating.set(self.outdict['Global evaluation'][0]) |
| 107 except KeyError: |
| 108 self.gui.rating.set('Error') |
| 109 self.gui.refresh_results_window() |
| 110 |
| 111 #reset stream variables for next run |
| 112 self.contents = [] |
| 113 self.outdict = {} |
| 114 self.currout = None |
| 115 self.next_title = None |
| 116 |
| 117 |
| 118 class LintGui(object): |
| 119 """Build and control a window to interact with pylint""" |
| 120 |
| 121 def __init__(self, root=None): |
| 122 """init""" |
| 123 self.root = root or Tk() |
| 124 self.root.title('Pylint') |
| 125 #reporter |
| 126 self.reporter = None |
| 127 #message queue for output from reporter |
| 128 self.msg_queue = six.moves.queue.Queue() |
| 129 self.msgs = [] |
| 130 self.visible_msgs = [] |
| 131 self.filenames = [] |
| 132 self.rating = StringVar() |
| 133 self.tabs = {} |
| 134 self.report_stream = BasicStream(self) |
| 135 #gui objects |
| 136 self.lb_messages = None |
| 137 self.showhistory = None |
| 138 self.results = None |
| 139 self.btnRun = None |
| 140 self.information_box = None |
| 141 self.convention_box = None |
| 142 self.refactor_box = None |
| 143 self.warning_box = None |
| 144 self.error_box = None |
| 145 self.fatal_box = None |
| 146 self.txtModule = None |
| 147 self.status = None |
| 148 self.msg_type_dict = None |
| 149 self.init_gui() |
| 150 |
| 151 def init_gui(self): |
| 152 """init helper""" |
| 153 |
| 154 window = PanedWindow(self.root, orient="vertical") |
| 155 window.pack(side=TOP, fill=BOTH, expand=True) |
| 156 |
| 157 top_pane = Frame(window) |
| 158 window.add(top_pane) |
| 159 mid_pane = Frame(window) |
| 160 window.add(mid_pane) |
| 161 bottom_pane = Frame(window) |
| 162 window.add(bottom_pane) |
| 163 |
| 164 #setting up frames |
| 165 top_frame = Frame(top_pane) |
| 166 mid_frame = Frame(top_pane) |
| 167 history_frame = Frame(top_pane) |
| 168 radio_frame = Frame(mid_pane) |
| 169 rating_frame = Frame(mid_pane) |
| 170 res_frame = Frame(mid_pane) |
| 171 check_frame = Frame(bottom_pane) |
| 172 msg_frame = Frame(bottom_pane) |
| 173 btn_frame = Frame(bottom_pane) |
| 174 top_frame.pack(side=TOP, fill=X) |
| 175 mid_frame.pack(side=TOP, fill=X) |
| 176 history_frame.pack(side=TOP, fill=BOTH, expand=True) |
| 177 radio_frame.pack(side=TOP, fill=X) |
| 178 rating_frame.pack(side=TOP, fill=X) |
| 179 res_frame.pack(side=TOP, fill=BOTH, expand=True) |
| 180 check_frame.pack(side=TOP, fill=X) |
| 181 msg_frame.pack(side=TOP, fill=BOTH, expand=True) |
| 182 btn_frame.pack(side=TOP, fill=X) |
| 183 |
| 184 # Binding F5 application-wide to run lint |
| 185 self.root.bind('<F5>', self.run_lint) |
| 186 |
| 187 #Message ListBox |
| 188 rightscrollbar = Scrollbar(msg_frame) |
| 189 rightscrollbar.pack(side=RIGHT, fill=Y) |
| 190 bottomscrollbar = Scrollbar(msg_frame, orient=HORIZONTAL) |
| 191 bottomscrollbar.pack(side=BOTTOM, fill=X) |
| 192 self.lb_messages = Listbox( |
| 193 msg_frame, |
| 194 yscrollcommand=rightscrollbar.set, |
| 195 xscrollcommand=bottomscrollbar.set, |
| 196 bg="white") |
| 197 self.lb_messages.bind("<Double-Button-1>", self.show_sourcefile) |
| 198 self.lb_messages.pack(expand=True, fill=BOTH) |
| 199 rightscrollbar.config(command=self.lb_messages.yview) |
| 200 bottomscrollbar.config(command=self.lb_messages.xview) |
| 201 |
| 202 #History ListBoxes |
| 203 rightscrollbar2 = Scrollbar(history_frame) |
| 204 rightscrollbar2.pack(side=RIGHT, fill=Y) |
| 205 bottomscrollbar2 = Scrollbar(history_frame, orient=HORIZONTAL) |
| 206 bottomscrollbar2.pack(side=BOTTOM, fill=X) |
| 207 self.showhistory = Listbox( |
| 208 history_frame, |
| 209 yscrollcommand=rightscrollbar2.set, |
| 210 xscrollcommand=bottomscrollbar2.set, |
| 211 bg="white") |
| 212 self.showhistory.pack(expand=True, fill=BOTH) |
| 213 rightscrollbar2.config(command=self.showhistory.yview) |
| 214 bottomscrollbar2.config(command=self.showhistory.xview) |
| 215 self.showhistory.bind('<Double-Button-1>', self.select_recent_file) |
| 216 self.set_history_window() |
| 217 |
| 218 #status bar |
| 219 self.status = Label(self.root, text="", bd=1, relief=SUNKEN, anchor=W) |
| 220 self.status.pack(side=BOTTOM, fill=X) |
| 221 |
| 222 #labelbl_ratingls |
| 223 lbl_rating_label = Label(rating_frame, text='Rating:') |
| 224 lbl_rating_label.pack(side=LEFT) |
| 225 lbl_rating = Label(rating_frame, textvariable=self.rating) |
| 226 lbl_rating.pack(side=LEFT) |
| 227 Label(mid_frame, text='Recently Used:').pack(side=LEFT) |
| 228 Label(top_frame, text='Module or package').pack(side=LEFT) |
| 229 |
| 230 #file textbox |
| 231 self.txt_module = Entry(top_frame, background='white') |
| 232 self.txt_module.bind('<Return>', self.run_lint) |
| 233 self.txt_module.pack(side=LEFT, expand=True, fill=X) |
| 234 |
| 235 #results box |
| 236 rightscrollbar = Scrollbar(res_frame) |
| 237 rightscrollbar.pack(side=RIGHT, fill=Y) |
| 238 bottomscrollbar = Scrollbar(res_frame, orient=HORIZONTAL) |
| 239 bottomscrollbar.pack(side=BOTTOM, fill=X) |
| 240 self.results = Listbox( |
| 241 res_frame, |
| 242 yscrollcommand=rightscrollbar.set, |
| 243 xscrollcommand=bottomscrollbar.set, |
| 244 bg="white", font="Courier") |
| 245 self.results.pack(expand=True, fill=BOTH, side=BOTTOM) |
| 246 rightscrollbar.config(command=self.results.yview) |
| 247 bottomscrollbar.config(command=self.results.xview) |
| 248 |
| 249 #buttons |
| 250 Button(top_frame, text='Open', command=self.file_open).pack(side=LEFT) |
| 251 Button(top_frame, text='Open Package', |
| 252 command=(lambda: self.file_open(package=True))).pack(side=LEFT) |
| 253 |
| 254 self.btnRun = Button(top_frame, text='Run', command=self.run_lint) |
| 255 self.btnRun.pack(side=LEFT) |
| 256 Button(btn_frame, text='Quit', command=self.quit).pack(side=BOTTOM) |
| 257 |
| 258 #radio buttons |
| 259 self.information_box = IntVar() |
| 260 self.convention_box = IntVar() |
| 261 self.refactor_box = IntVar() |
| 262 self.warning_box = IntVar() |
| 263 self.error_box = IntVar() |
| 264 self.fatal_box = IntVar() |
| 265 i = Checkbutton(check_frame, text="Information", fg=COLORS['(I)'], |
| 266 variable=self.information_box, command=self.refresh_msg_
window) |
| 267 c = Checkbutton(check_frame, text="Convention", fg=COLORS['(C)'], |
| 268 variable=self.convention_box, command=self.refresh_msg_w
indow) |
| 269 r = Checkbutton(check_frame, text="Refactor", fg=COLORS['(R)'], |
| 270 variable=self.refactor_box, command=self.refresh_msg_win
dow) |
| 271 w = Checkbutton(check_frame, text="Warning", fg=COLORS['(W)'], |
| 272 variable=self.warning_box, command=self.refresh_msg_wind
ow) |
| 273 e = Checkbutton(check_frame, text="Error", fg=COLORS['(E)'], |
| 274 variable=self.error_box, command=self.refresh_msg_window
) |
| 275 f = Checkbutton(check_frame, text="Fatal", fg=COLORS['(F)'], |
| 276 variable=self.fatal_box, command=self.refresh_msg_window
) |
| 277 i.select() |
| 278 c.select() |
| 279 r.select() |
| 280 w.select() |
| 281 e.select() |
| 282 f.select() |
| 283 i.pack(side=LEFT) |
| 284 c.pack(side=LEFT) |
| 285 r.pack(side=LEFT) |
| 286 w.pack(side=LEFT) |
| 287 e.pack(side=LEFT) |
| 288 f.pack(side=LEFT) |
| 289 |
| 290 #check boxes |
| 291 self.box = StringVar() |
| 292 # XXX should be generated |
| 293 report = Radiobutton( |
| 294 radio_frame, text="Report", variable=self.box, |
| 295 value="Report", command=self.refresh_results_window) |
| 296 raw_met = Radiobutton( |
| 297 radio_frame, text="Raw metrics", variable=self.box, |
| 298 value="Raw metrics", command=self.refresh_results_window) |
| 299 dup = Radiobutton( |
| 300 radio_frame, text="Duplication", variable=self.box, |
| 301 value="Duplication", command=self.refresh_results_window) |
| 302 ext = Radiobutton( |
| 303 radio_frame, text="External dependencies", |
| 304 variable=self.box, value="External dependencies", |
| 305 command=self.refresh_results_window) |
| 306 stat = Radiobutton( |
| 307 radio_frame, text="Statistics by type", |
| 308 variable=self.box, value="Statistics by type", |
| 309 command=self.refresh_results_window) |
| 310 msg_cat = Radiobutton( |
| 311 radio_frame, text="Messages by category", |
| 312 variable=self.box, value="Messages by category", |
| 313 command=self.refresh_results_window) |
| 314 msg = Radiobutton( |
| 315 radio_frame, text="Messages", variable=self.box, |
| 316 value="Messages", command=self.refresh_results_window) |
| 317 source_file = Radiobutton( |
| 318 radio_frame, text="Source File", variable=self.box, |
| 319 value="Source File", command=self.refresh_results_window) |
| 320 report.select() |
| 321 report.grid(column=0, row=0, sticky=W) |
| 322 raw_met.grid(column=1, row=0, sticky=W) |
| 323 dup.grid(column=2, row=0, sticky=W) |
| 324 msg.grid(column=3, row=0, sticky=W) |
| 325 stat.grid(column=0, row=1, sticky=W) |
| 326 msg_cat.grid(column=1, row=1, sticky=W) |
| 327 ext.grid(column=2, row=1, sticky=W) |
| 328 source_file.grid(column=3, row=1, sticky=W) |
| 329 |
| 330 #dictionary for check boxes and associated error term |
| 331 self.msg_type_dict = { |
| 332 'I': lambda: self.information_box.get() == 1, |
| 333 'C': lambda: self.convention_box.get() == 1, |
| 334 'R': lambda: self.refactor_box.get() == 1, |
| 335 'E': lambda: self.error_box.get() == 1, |
| 336 'W': lambda: self.warning_box.get() == 1, |
| 337 'F': lambda: self.fatal_box.get() == 1 |
| 338 } |
| 339 self.txt_module.focus_set() |
| 340 |
| 341 |
| 342 def select_recent_file(self, event): # pylint: disable=unused-argument |
| 343 """adds the selected file in the history listbox to the Module box""" |
| 344 if not self.showhistory.size(): |
| 345 return |
| 346 |
| 347 selected = self.showhistory.curselection() |
| 348 item = self.showhistory.get(selected) |
| 349 #update module |
| 350 self.txt_module.delete(0, END) |
| 351 self.txt_module.insert(0, item) |
| 352 |
| 353 def refresh_msg_window(self): |
| 354 """refresh the message window with current output""" |
| 355 #clear the window |
| 356 self.lb_messages.delete(0, END) |
| 357 self.visible_msgs = [] |
| 358 for msg in self.msgs: |
| 359 if self.msg_type_dict.get(msg.C)(): |
| 360 self.visible_msgs.append(msg) |
| 361 msg_str = convert_to_string(msg) |
| 362 self.lb_messages.insert(END, msg_str) |
| 363 fg_color = COLORS.get(msg_str[:3], 'black') |
| 364 self.lb_messages.itemconfigure(END, fg=fg_color) |
| 365 |
| 366 def refresh_results_window(self): |
| 367 """refresh the results window with current output""" |
| 368 #clear the window |
| 369 self.results.delete(0, END) |
| 370 try: |
| 371 for res in self.tabs[self.box.get()]: |
| 372 self.results.insert(END, res) |
| 373 except KeyError: |
| 374 pass |
| 375 |
| 376 def process_incoming(self): |
| 377 """process the incoming messages from running pylint""" |
| 378 while self.msg_queue.qsize(): |
| 379 try: |
| 380 msg = self.msg_queue.get(0) |
| 381 if msg == "DONE": |
| 382 self.report_stream.output_contents() |
| 383 return False |
| 384 |
| 385 #adding message to list of msgs |
| 386 self.msgs.append(msg) |
| 387 |
| 388 #displaying msg if message type is selected in check box |
| 389 if self.msg_type_dict.get(msg.C)(): |
| 390 self.visible_msgs.append(msg) |
| 391 msg_str = convert_to_string(msg) |
| 392 self.lb_messages.insert(END, msg_str) |
| 393 fg_color = COLORS.get(msg_str[:3], 'black') |
| 394 self.lb_messages.itemconfigure(END, fg=fg_color) |
| 395 |
| 396 except six.moves.queue.Empty: |
| 397 pass |
| 398 return True |
| 399 |
| 400 def periodic_call(self): |
| 401 """determine when to unlock the run button""" |
| 402 if self.process_incoming(): |
| 403 self.root.after(100, self.periodic_call) |
| 404 else: |
| 405 #enabling button so it can be run again |
| 406 self.btnRun.config(state=NORMAL) |
| 407 |
| 408 def mainloop(self): |
| 409 """launch the mainloop of the application""" |
| 410 self.root.mainloop() |
| 411 |
| 412 def quit(self, _=None): |
| 413 """quit the application""" |
| 414 self.root.quit() |
| 415 |
| 416 def halt(self): # pylint: disable=no-self-use |
| 417 """program halt placeholder""" |
| 418 return |
| 419 |
| 420 def file_open(self, package=False, _=None): |
| 421 """launch a file browser""" |
| 422 if not package: |
| 423 filename = askopenfilename(parent=self.root, |
| 424 filetypes=[('pythonfiles', '*.py'), |
| 425 ('allfiles', '*')], |
| 426 title='Select Module') |
| 427 else: |
| 428 filename = askdirectory(title="Select A Folder", mustexist=1) |
| 429 |
| 430 if filename == (): |
| 431 return |
| 432 |
| 433 self.txt_module.delete(0, END) |
| 434 self.txt_module.insert(0, filename) |
| 435 |
| 436 def update_filenames(self): |
| 437 """update the list of recent filenames""" |
| 438 filename = self.txt_module.get() |
| 439 if not filename: |
| 440 filename = os.getcwd() |
| 441 if filename+'\n' in self.filenames: |
| 442 index = self.filenames.index(filename+'\n') |
| 443 self.filenames.pop(index) |
| 444 |
| 445 #ensure only 10 most recent are stored |
| 446 if len(self.filenames) == 10: |
| 447 self.filenames.pop() |
| 448 self.filenames.insert(0, filename+'\n') |
| 449 |
| 450 def set_history_window(self): |
| 451 """update the history window with info from the history file""" |
| 452 #clear the window |
| 453 self.showhistory.delete(0, END) |
| 454 # keep the last 10 most recent files |
| 455 try: |
| 456 view_history = open(HOME+HISTORY, 'r') |
| 457 for hist in view_history.readlines(): |
| 458 if not hist in self.filenames: |
| 459 self.filenames.append(hist) |
| 460 self.showhistory.insert(END, hist.split('\n')[0]) |
| 461 view_history.close() |
| 462 except IOError: |
| 463 # do nothing since history file will be created later |
| 464 return |
| 465 |
| 466 def run_lint(self, _=None): |
| 467 """launches pylint""" |
| 468 self.update_filenames() |
| 469 self.root.configure(cursor='watch') |
| 470 self.reporter = GUIReporter(self, output=self.report_stream) |
| 471 module = self.txt_module.get() |
| 472 if not module: |
| 473 module = os.getcwd() |
| 474 |
| 475 #cleaning up msgs and windows |
| 476 self.msgs = [] |
| 477 self.visible_msgs = [] |
| 478 self.lb_messages.delete(0, END) |
| 479 self.tabs = {} |
| 480 self.results.delete(0, END) |
| 481 self.btnRun.config(state=DISABLED) |
| 482 |
| 483 #setting up a worker thread to run pylint |
| 484 worker = Thread(target=lint_thread, args=(module, self.reporter, self,)) |
| 485 self.periodic_call() |
| 486 worker.start() |
| 487 |
| 488 # Overwrite the .pylint-gui-history file with all the new recently added
files |
| 489 # in order from filenames but only save last 10 files |
| 490 write_history = open(HOME+HISTORY, 'w') |
| 491 write_history.writelines(self.filenames) |
| 492 write_history.close() |
| 493 self.set_history_window() |
| 494 |
| 495 self.root.configure(cursor='') |
| 496 |
| 497 def show_sourcefile(self, event=None): # pylint: disable=unused-argument |
| 498 selected = self.lb_messages.curselection() |
| 499 if not selected: |
| 500 return |
| 501 |
| 502 msg = self.visible_msgs[int(selected[0])] |
| 503 scroll = msg.line - 3 |
| 504 if scroll < 0: |
| 505 scroll = 0 |
| 506 |
| 507 self.tabs["Source File"] = open(msg.path, "r").readlines() |
| 508 self.box.set("Source File") |
| 509 self.refresh_results_window() |
| 510 self.results.yview(scroll) |
| 511 self.results.select_set(msg.line - 1) |
| 512 |
| 513 |
| 514 def lint_thread(module, reporter, gui): |
| 515 """thread for pylint""" |
| 516 gui.status.text = "processing module(s)" |
| 517 pylint.lint.Run(args=[module], reporter=reporter, exit=False) |
| 518 gui.msg_queue.put("DONE") |
| 519 |
| 520 |
| 521 def Run(args): |
| 522 """launch pylint gui from args""" |
| 523 if args: |
| 524 print('USAGE: pylint-gui\n launch a simple pylint gui using Tk') |
| 525 sys.exit(1) |
| 526 gui = LintGui() |
| 527 gui.mainloop() |
| 528 sys.exit(0) |
| 529 |
| 530 if __name__ == '__main__': |
| 531 Run(sys.argv[1:]) |
OLD | NEW |