# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:[email protected]
#
# 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:])