250 lines
8.5 KiB
Python
250 lines
8.5 KiB
Python
"""
|
|
Code Counter
|
|
A program that counts the lines of code and code characters in a code-base
|
|
Author : Israel Dryer
|
|
Modified : 2019-11-01
|
|
You can find the original repository with the latest updates here:
|
|
https://github.com/israel-dryer/Code-Counter
|
|
|
|
"""
|
|
import PySimpleGUI as sg
|
|
import statistics as stats
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
|
|
WINDOW_SIZE = (1280, 720)
|
|
|
|
def clean_data(window):
|
|
""" clean and parse the raw data """
|
|
raw = window.AllKeysDict['INPUT'].DefaultText.split('\n')
|
|
# remove whitespace
|
|
data = [row.strip() for row in raw if row.strip()]
|
|
|
|
# remove hash comments
|
|
stage1 = []
|
|
for row in data:
|
|
if row.find('#') != -1:
|
|
stage1.append(row[:row.find('#')])
|
|
else:
|
|
stage1.append(row)
|
|
|
|
# remove " multiline comments
|
|
stage2 = []
|
|
ml_flag = False # multiline comment flag
|
|
for row in stage1:
|
|
if row.count(r'"""') == 0 and not ml_flag: # not a comment line
|
|
stage2.append(row)
|
|
elif row.count(r'"""') == 1 and not ml_flag: # starting comment line
|
|
ml_flag = True
|
|
stage2.append(row[:row.find('"""')])
|
|
elif row.count(r'"""') == 1 and ml_flag: # ending comment line
|
|
ml_flag = False
|
|
stage2.append(row[row.find('"""') + 1:])
|
|
else:
|
|
continue
|
|
|
|
# remove ' multiline comments
|
|
stage3 = []
|
|
ml_flag = False # multiline comment flag
|
|
for row in stage2:
|
|
if row.count(r"'''") == 0 and not ml_flag: # not a comment line
|
|
stage3.append(row)
|
|
elif row.count(r"'''") == 1 and not ml_flag: # starting comment line
|
|
ml_flag = True
|
|
stage3.append(row[:row.find("'''")])
|
|
elif row.count(r"'''") == 1 and ml_flag: # ending comment line
|
|
ml_flag = False
|
|
stage3.append(row[row.find("'''") + 1:])
|
|
else:
|
|
continue
|
|
|
|
clean_code = [row for row in stage3 if row not in ('', "''", '""')]
|
|
|
|
# row and character rounds / for calc stats, histogram, charts
|
|
char_cnt = [len(row) for row in clean_code]
|
|
|
|
# statistics
|
|
if len(clean_code) == 0:
|
|
char_per_line = 1
|
|
else:
|
|
char_per_line = sum(char_cnt) // len(clean_code)
|
|
code_stats = {
|
|
'lines': len(clean_code), 'char_per_line': char_per_line,
|
|
'count': sum(char_cnt), 'mean': stats.mean(char_cnt), 'median': stats.median(char_cnt),
|
|
'pstdev': stats.pstdev(char_cnt), 'min': min(char_cnt), 'max': max(char_cnt)}
|
|
|
|
return clean_code, char_cnt, code_stats
|
|
|
|
|
|
def process_data(window):
|
|
""" clean and save data ... previous executed manually with submit button """
|
|
# try:
|
|
clean_code, char_cnt, code_stats = clean_data(window)
|
|
save_data(clean_code, code_stats, window)
|
|
display_charts(char_cnt, window)
|
|
display_stats(code_stats, window)
|
|
window['T2'].select()
|
|
|
|
|
|
def save_data(clean_code, code_stats, window):
|
|
window['OUTPUT'].update('\n'.join([row for row in clean_code]))
|
|
return
|
|
""" save clean code and stats to file """
|
|
with open('output.txt', 'w') as f:
|
|
for row in clean_code:
|
|
f.write(row + '\n')
|
|
|
|
# update display
|
|
with open('output.txt', 'r') as f:
|
|
window['OUTPUT'].update(f.read())
|
|
|
|
|
|
def display_charts(char_cnt, window):
|
|
""" create charts to display in window """
|
|
|
|
def draw_figure(canvas, figure, loc=(0, 0)):
|
|
""" matplotlib helper function """
|
|
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
|
|
figure_canvas_agg.draw()
|
|
figure_canvas_agg.get_tk_widget().pack()
|
|
return figure_canvas_agg
|
|
|
|
figure = plt.figure(num=1, figsize=(4, 5))
|
|
|
|
# histogram
|
|
plt.subplot(211)
|
|
plt.hist(char_cnt)
|
|
plt.title('character count per line')
|
|
plt.ylabel('frequency')
|
|
plt.tight_layout()
|
|
|
|
# line plot
|
|
plt.subplot(212)
|
|
x = range(0, len(char_cnt))
|
|
y = char_cnt
|
|
plt.plot(y)
|
|
plt.fill_between(x, y)
|
|
plt.title('compressed code line counts')
|
|
plt.xlabel('code line number')
|
|
plt.ylabel('number of characters')
|
|
plt.tight_layout()
|
|
draw_figure(window['IMG'].TKCanvas, figure)
|
|
|
|
|
|
def display_stats(code_stats, window):
|
|
""" display code stats in the window """
|
|
window['LINES'].update('{:,d}'.format(code_stats['lines']))
|
|
window['CHARS'].update('{:,d}'.format(code_stats['count']))
|
|
window['CPL'].update('{:,d}'.format(code_stats['char_per_line']))
|
|
window['MEAN'].update('{:,.0f}'.format(code_stats['mean']))
|
|
window['MEDIAN'].update('{:,.0f}'.format(code_stats['median']))
|
|
window['PSTDEV'].update('{:,.0f}'.format(code_stats['pstdev']))
|
|
window['MAX'].update('{:,d}'.format(code_stats['max']))
|
|
window['MIN'].update('{:,d}'.format(code_stats['min']))
|
|
|
|
|
|
def click_file(window):
|
|
""" file button click event; open file and load to screen """
|
|
filename = sg.popup_get_file('Select a file containing Python code:', title='Code Counter')
|
|
if filename is None:
|
|
return
|
|
with open(filename) as f:
|
|
raw = f.read()
|
|
window['INPUT'].update(raw)
|
|
|
|
|
|
def click_clipboard(window):
|
|
""" get data from clipboard and paste to input """
|
|
try:
|
|
clip = window['INPUT'].Widget.clipboard_get()
|
|
window['INPUT'].update(clip)
|
|
except:
|
|
sg.popup_error('Clipboard is empty', no_titlebar=True)
|
|
|
|
|
|
def click_reset(window):
|
|
""" reset the windows and data fields """
|
|
window['INPUT'].update('')
|
|
window['OUTPUT'].update('')
|
|
reset_stats(window)
|
|
window['T1'].select()
|
|
|
|
|
|
def reset_stats(window):
|
|
""" clear the stats fields """
|
|
window['LINES'].update('{:,d}'.format(0))
|
|
window['CHARS'].update('{:,d}'.format(0))
|
|
window['CPL'].update('{:,d}'.format(0))
|
|
window['MEAN'].update('{:,.0f}'.format(0))
|
|
window['MEDIAN'].update('{:,.0f}'.format(0))
|
|
window['PSTDEV'].update('{:,.0f}'.format(0))
|
|
window['MAX'].update('{:,d}'.format(0))
|
|
window['MIN'].update('{:,d}'.format(0))
|
|
|
|
def btn(name, **kwargs):
|
|
""" create button with default settings """
|
|
return sg.Button(name, size=(16, 1), font=(sg.DEFAULT_FONT, 12), **kwargs)
|
|
|
|
def stat(text, width=10, relief=None, justification='left', key=None):
|
|
elem = sg.Text(text, size=(width, 1), relief=relief, justification=justification, key=key)
|
|
return elem
|
|
|
|
|
|
def main():
|
|
""" main program and GUI loop """
|
|
sg.ChangeLookAndFeel('BrownBlue')
|
|
|
|
tab1 = sg.Tab('Raw Code',
|
|
[[sg.Multiline(key='INPUT', pad=(0, 0), font=(sg.DEFAULT_FONT, 12))]],
|
|
background_color='gray', key='T1')
|
|
tab2 = sg.Tab('Clean Code',
|
|
[[sg.Multiline(key='OUTPUT', pad=(0, 0), font=(sg.DEFAULT_FONT, 12))]],
|
|
background_color='gray25', key='T2')
|
|
|
|
stat_col = sg.Column([
|
|
[stat('Lines of code'), stat(0, 8, 'sunken', 'right', 'LINES'),
|
|
stat('Total chars'), stat(0, 8, 'sunken', 'right', 'CHARS')],
|
|
[stat('Chars per line'), stat(0, 8, 'sunken', 'right', 'CPL'),
|
|
stat('Mean'), stat(0, 8, 'sunken', 'right', 'MEAN')],
|
|
[stat('Median'), stat(0, 8, 'sunken', 'right', 'MEDIAN'),
|
|
stat('PStDev'), stat(0, 8, 'sunken', 'right', 'PSTDEV')],
|
|
[stat('Max'), stat(0, 8, 'sunken', 'right', 'MAX'),
|
|
stat('Min'), stat(0, 8, 'sunken', 'right', 'MIN')]], pad=(5, 10), key='STATS')
|
|
|
|
lf_col = [
|
|
[btn('Load FILE'), btn('Clipboard'), btn('RESET')],
|
|
[sg.TabGroup([[tab1, tab2]], title_color='black', key='TABGROUP')]]
|
|
|
|
rt_col = [
|
|
[sg.Text('LOAD a file or PASTE code from Clipboard', pad=(5, 15))],
|
|
[sg.Text('Statistics', size=(20, 1), pad=((5, 5), (15, 5)),
|
|
font=(sg.DEFAULT_FONT, 14, 'bold'), justification='center')],
|
|
[stat_col],
|
|
[sg.Text('Visualization', size=(20, 1),
|
|
font=(sg.DEFAULT_FONT, 14, 'bold'), justification='center')],
|
|
[sg.Canvas(key='IMG')]]
|
|
|
|
layout = [[sg.Column(lf_col, element_justification='left', pad=(0, 10), key='LCOL'),
|
|
sg.Column(rt_col, element_justification='center', key='RCOL')]]
|
|
|
|
window = sg.Window('Code Counter', layout, resizable=True, size=WINDOW_SIZE, finalize=True)
|
|
|
|
for elem in ['INPUT', 'OUTPUT', 'LCOL', 'TABGROUP']:
|
|
window[elem].expand(expand_x=True, expand_y=True)
|
|
|
|
# main event loop
|
|
while True:
|
|
event, values = window.read()
|
|
if event is None:
|
|
break
|
|
if event == 'Load FILE':
|
|
click_file(window)
|
|
process_data(window)
|
|
if event == 'Clipboard':
|
|
click_clipboard(window)
|
|
process_data(window)
|
|
if event == 'RESET':
|
|
click_reset(window)
|
|
|
|
if __name__ == '__main__':
|
|
main() |