Skip to content

Instantly share code, notes, and snippets.

@0xpizza
Last active January 22, 2021 20:16
Show Gist options
  • Select an option

  • Save 0xpizza/5518eaee784d9e6d8e8670df21d23d1e to your computer and use it in GitHub Desktop.

Select an option

Save 0xpizza/5518eaee784d9e6d8e8670df21d23d1e to your computer and use it in GitHub Desktop.
bc on steroids
import ast
import math
import cmath
import itertools
import functools
import statistics
import tkinter as tk
from tkinter.ttk import Progressbar
from tkinter.scrolledtext import ScrolledText
import concurrent.futures
NAMESPACE_MATH = {
f:eval(f'math.{f}') for f in dir(math) if not f.startswith('_')
}
NAMESPACE_CMATH = {
f:eval(f'cmath.{f}') for f in dir(cmath) if not f.startswith('_')
}
NAMESPACE_STATISTICS = {
f:eval(f'statistics.{f}') for f in dir(statistics) if not f.startswith('_')
}
NAMESPACE_ITERTOOLS = {
f:eval(f'itertools.{f}') for f in dir(itertools) if not f.startswith('_')
}
NAMESPACE_BUILTINS = {
f:eval(f) for f in (
'abs bin chr complex divmod float hex int len '
'list map max min oct ord pow range round sum zip'
).split()
}
NAMESPACE_BUILTINS['__builtins__'] = None
def safe_eval(expr, whitelist) -> object:
"""Evaluate an expression in a way that attempts to
eliminate the possibility of arbitrary code execution.
Only the basic data and object types are available, and
the user's ability to create arbitray objects is severly
hindered (and hopefully eliminated).
"""
# all errors are homogenized as ValueErrors to simplify upstream code
if len(expr) > 100:
raise ValueError('Expression is too long.')
try:
nodes = ast.parse(expr)
except SyntaxError as e:
raise ValueError('Invalid syntax')
for node in ast.walk(nodes):
if isinstance(node, ast.Attribute):
raise ValueError('Attribute access not allowed')
try:
return eval(expr, {**whitelist, **NAMESPACE_BUILTINS}, {})
except (TypeError, SyntaxError):
raise ValueError('Invalid expression')
except NameError as e:
raise ValueError(''.join(e.args))
class ReadOnlyText(ScrolledText):
"""Text widget that can only be modified programatically.
"""
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.insert = self._unlock(super().insert)
self.delete = self._unlock(super().delete)
self.configure(state=tk.DISABLED)
def _unlock(self, f):
@functools.wraps(f)
def _wrap(*args, **kwargs):
nonlocal self
nonlocal f
self.configure(state=tk.NORMAL)
r = f(*args, **kwargs)
self.configure(state=tk.DISABLED)
return r
return _wrap
class CalcDisplay(ReadOnlyText):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.config(
height=1, width=1, background='black',
foreground='lime', borderwidth=0
)
self.vbar.pack_forget() # hide the scrollbar
self.pack(fill=tk.BOTH, expand=tk.YES, padx=5, pady=5)
self.tag_config('cjust', justify=tk.CENTER)
self.tag_config('ljust', justify=tk.LEFT)
self.tag_config('rjust', justify=tk.RIGHT, background='#000d00')
def insert(self, *args):
"""This method doesn't ever get called for some reason..."""
super().insert(*args)
self.see(tk.END)
def write_l(self, text):
self.insert(tk.END, text, 'ljust')
self.see(tk.END)
def write_r(self, text):
self.insert(tk.END, text, 'rjust')
self.see(tk.END)
def write_c(self, text):
self.insert(tk.END, text, 'cjust')
self.see(tk.END)
class CalcInput(tk.Text):
def __init__(self, master=None, **kwargs):
self.submit_cb = kwargs.pop('submitcallback')
super().__init__(master, **kwargs)
self.config(
background='black',foreground='lime', #relief=tk.FLAT,
borderwidth=1, highlightbackground='white', height=1,
insertbackground='lime'
)
self.pack(fill=tk.X)
self.focus_set()
self.seekable = False
self.allow_submit = tk.IntVar(self)
self.shift_position = tk.IntVar(self) # 1 = down, 0 = up
self.bind('<Return>', self.submit)
# Use Shift + Enter to insert a literal newline
self.bind('<BackSpace>', self.check_height)
self.bind('<Delete>', self.check_height)
self.bind('<Shift-Return>', self.check_height)
self.bind('<KeyPress-Shift_L>', self.shift_depressed)
self.bind('<KeyRelease-Shift_L>', self.shift_released)
# Ctrl + C clears input field
self.bind('<Control-c>', functools.partial(
self.after, 1, (lambda _:self.delete(1.0, tk.END))
))
# Use the up arrow key to copy the last expression
#self.bind('<Up>', self.set_last_expression)
#TODO: figure out the height handler.
def shift_depressed(self, event=None):
self.shift_position.set(1)
self.allow_submit.set(0)
def shift_released(self, event=None):
self.shift_position.set(0)
self.allow_submit.set(1)
self.check_height()
def submit(self, event=None):
if self.allow_submit.get():
txt = self.get(1.0, tk.END)
self.last_expression = txt
self.current_expression = ''
self.configure(height=1)
self.delete(1.0, tk.END)
self.submit_cb(txt)
self.check_height()
def check_height(self, event=None):
"""Buffer update happens after keypresses are processed,
so basically all content checks are 1 input cycle behind.
We can force the buffers to update before the keyboard
events by cycling the event loop once before checking.
"""
self.after(1, self._check_height)
def _check_height(self):
h = (
int(self.index(tk.END).split('.')[0]) -
self.get(1.0, tk.END).count('\n',-2)
)
print(h, self.get(1.0, 'end').encode())
if h < 1:
h = 1
if h > 1:
self.seekable = True
else:
self.seekable = False
self.configure(height=h)
class Calculator(tk.Frame):
"""A GUI calculator with a command line interface.
Calculations are sent off to another process where
they can be computed safely without the overhead
of the GUI, and can be cancelled if needed.
The calculator also
"""
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.config(background='black')
# TODO: add ability to switch modes
self.modes = {
'math': NAMESPACE_MATH,
'cmath': NAMESPACE_CMATH,
'stats':NAMESPACE_STATISTICS
}
# special variable representing the result of the last computation
self.ANS = 0
self.stored_answers = {
chr(c):0 for c in range(65,91)
}
self.current_mode = self.modes.get('math')
self.display = CalcDisplay(self)
self.text_input = CalcInput(self, submitcallback=self.submit)
# initialize the process pool asynchronously to improve startup time
# TODO: use multiprocessing for performance and job cancellation
self._init_m = concurrent.futures.ThreadPoolExecutor()
self.display.write_c('\n>>> CalcuPy <<<\nsee /? or /help\n')
def parse_command(self, expr):
return 'No commands yet. Carry on!'
def submit(self, expr):
"""Handle input from text_input, compute the answer, then
display the input in the display widget.
"""
expr = expr.strip()
# process as an application command
if expr.startswith('/'):
result = self.parse_command(expr)
self.display.write_l(expr + '\n')
self.display.write_l(result)
return
# attempt proprocess a storage variable
var = None
if expr[1:2] == '=':
if not (var:=ord(expr[:1])) in range(65,91):
expr = expr[2:]
if not expr:
self.write_r(var + ' cleared')
return
else:
self.write_l('No such variable. See /? for more information')
return
# remove all unecessary whitespace
#(sadly, this inflates the length of legit strings :/)
oneliner = ' '.join(
filter(bool,
map(str.strip,
expr.split()
)))
try:
result = safe_eval(
oneliner, {
**self.current_mode,
**NAMESPACE_ITERTOOLS,
**self.stored_answers,
'ANS':self.ANS
})
self.ANS = result
self.stored_answers[var] = result
except ValueError as e:
result = ''.join(e.args)
self.display.write_l(expr)
self.display.write_l('\n')
self.display.write_r(result)
self.display.write_l('\n')
class Root(tk.Tk):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.geometry('400x300')
self.bind('<Escape>', lambda *_:self.destroy())
Calculator(self).pack(fill=tk.BOTH, expand=tk.YES)
def main():
Root().mainloop()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment