Made setuptools package

trunk
alexis 2015-04-04 12:22:47 -04:00
parent 764d6b5a99
commit 43b9d981a3
5 changed files with 1008 additions and 984 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.pyc
*.pyo
*.egg-info
build
dist

986
LerpnApp/__init__.py Executable file
View File

@ -0,0 +1,986 @@
#!/usr/bin/python
# Written in 2015 by Christopher Pavlina.
###############################################################################
# lerpn - Linux Engineering RPN calculator
#
# This is a simple RPN calculator using Python and curses. How to use it should
# be obvious for those familiar with RPN. The basic commands:
#
# + - * /
#
# enter dup
# x exchange/swap
# bksp drop
# ? display help
# ctrl-C exit
#
#
# Numbers are displayed in engineering notation with metric prefixes. You
# cannot TYPE them this way, however; that would cause too many collisions with
# single-key commands IMHO.
#
# Not all commands are a single key. Less-used commands begin with a
# single-quote, then the command name, then 'enter'. For example, the sequence
# of keys used to compute the sine of 3.14 would be:
#
# 3 . 1 4 <enter> ' s i n <enter>
#
# You can tag numbers to help remember what they are. To do this, first have
# the number at the end of the stack. Then, push a string to the stack, by
# typing a double-quote ", then the string text, then <enter>. Then, press
# lower-case t to combine the number and the tag.
#
# Basic math can be done to numbers while tagged, but if you perform a binary
# operation on two tagged numbers, lerpn won't know which tag to keep.
#
# To untag a number, use upper-case T to split the number from its tag. You can
# then drop the tag.
#
# This is currently the only application for strings on the stack. Lone strings
# are displayed in green to make them obvious - otherwise, you'll be mighty
# confused when 2 + 3 = 23 !
#
###############################################################################
# ADDING COMMANDS
#
# Grep this source for ADDING SINGLE-LETTER COMMANDS or ADDING NAMED COMMANDS.
#
###############################################################################
#
# This software is free. Do whatever the fuck you want with it. Copy it, change
# it, steal it, strip away this notice, print it and tear it into confetti to
# throw at random dudes on the street. Really, I don't care.
import collections
import curses
import math
import string
import os
import pipes
import re
import shlex
import subprocess
import sys
import traceback
# Py2/3 bits
if not hasattr (math, "log2"):
math.log2 = lambda x: math.log(x, 2)
# Hack warning!
# Pint takes too long to load for a general-purpose calculator that has to come
# up quickly. Since it's not needed until after the first line has been
# entered, I'm throwing up a loader in a thread. Blame nickjohnson if it's bad
import threading
pint_load_event = threading.Event()
pint=None
UREG=None
def pintloader():
global pint, _num_parser, UREG, _Q_
try:
import pint
UREG = pint.UnitRegistry ()
UREG.autoconvert_offset_to_baseunit = True
def _num_parser(x):
# Do not return int!
ret = UREG.parse_expression (x)
if isinstance (ret, int):
return float (ret)
else:
return ret
def _Q_(x):
return UREG.parse_expression (x)
pint_load_event.set ()
except ImportError:
pint = None
_num_parser = float
def _Q_(x):
return float (x.partition(" ")[0])
pint_load_event.set ()
def num_parser(x):
pint_load_event.wait()
if '"' in x:
x, delim, tag = x.partition('"')
else:
tag = None
parsed_num = _num_parser(x)
if tag is None:
return parsed_num
else:
return Tagged (parsed_num, tag.strip())
def Q_(*args, **kwargs):
pint_load_event.wait()
return _Q_(*args, **kwargs)
pL = threading.Thread(target=pintloader)
pL.start()
# Pyperclip is optional
try:
import pyperclip
except ImportError:
pyperclip = None
FORMAT="eng"
SIGFIGS=7
ERROR=None
class UndoStack (list):
"""Stack supporting an 'undo' action.
This is used as the main RPN stack. You can append() and pop() and [] just
like with a normal list. You can also use undopush() to push a duplicate of
the stack itself onto a "stack of stacks", and then undo() to restore that.
"""
def __init__ (self, v=None):
"""Initialize an UndoStack from an optional source list.
"""
if v is None:
list.__init__(self)
self.__sos = []
else:
list.__init__(self, v)
self.__sos = []
def undopush (self):
"""Save the current stack state to be restored later with undo()
"""
self.__sos.append (self[:])
def undo (self):
"""Restore the last saved undo state
"""
if len (self.__sos) > 1:
self[:] = self.__sos.pop ()
else:
self[:] = []
class Tagged (object):
"""Tagged number object.
This behaves like a number, but also contains a string tag. The values
are accessible at .num and .tag
"""
def __init__ (self, num, tag):
self.num = num
self.tag = tag
# Add operations to Tagged
ops = [
("add", lambda x,y: x+y),
("sub", lambda x,y: x-y),
("mul", lambda x,y: x*y),
("truediv", lambda x,y: x/y),
("mod", lambda x,y: x%y),
("pow", lambda x,y: x**y),
]
for opname, opfunc in ops:
# For the uninitiated, the default-argument parameters create a new
# scope for the variable, allowing passing each loop iteration's value
# to the closure instead of closing around the single final value.
def method (self, other, opfunc=opfunc):
if isinstance (other, Tagged):
# If both are tagged, just remove the tag.
return opfunc (self.num, other.num)
if not isinstance (other, float): return NotImplemented
return Tagged (opfunc (self.num, other), self.tag)
def rmethod (self, other, opfunc=opfunc):
if isinstance (other, Tagged):
# If both are tagged, just remove the tag.
return opfunc (other.num, self.num)
if not isinstance (other, float): return NotImplemented
return Tagged (opfunc (other, self.num), self.tag)
setattr (Tagged, "__%s__" % opname, method)
setattr (Tagged, "__r%s__" % opname, rmethod)
class CursesScrollBox (object):
"""Displays some text in a scroll box."""
def __init__ (self, width, height):
"""Initialize.
@param width - desired width: positive for absolute width, negative
for margin around window edge
@param height - desired height: positive for absolute height,
negative for margin around window edge
"""
self.w = width
self.h = height
self.title = ""
self.pos = 0
def set_text (self, text):
self.text = text
def set_title (self, title):
self.title = title
def show (self):
if self.w < 0:
self._w = curses.COLS + self.w
else:
self._w = self.w
if self.h < 0:
self._h = curses.LINES + self.h
else:
self._h = self.h
self._x = curses.COLS // 2 - self._w // 2
self._y = curses.LINES // 2 - self._h // 2
curses.curs_set (0)
curses.doupdate ()
self.win = curses.newwin (self._h, self._w, self._y, self._x)
while self._show ():
pass
curses.curs_set (1)
def _show (self):
self.win.clear ()
self.win.move (1, 2)
self.win.addstr (self.title)
self.win.keypad (1)
if isinstance(self.text, str):
lines = self.text.split("\n")
elif isinstance(self.text, list):
lines = self.text
else:
raise TypeError ("Text must be list or str")
view_lines = lines[self.pos:self.pos+self._h - 4]
for y, line in enumerate (view_lines):
self.win.move (y + 3, 2)
self.win.addstr (line.rstrip())
if self.pos > 0:
self.win.move (2, self._w - 3)
self.win.addstr ("+")
if self.pos + self._h < len (lines) + 4:
self.win.move (self._h - 2, self._w - 3)
self.win.addstr ("+")
scroll_limit = len (lines) - self._h // 2
self.win.border ()
try:
key = self.win.getch ()
except KeyboardInterrupt:
return False
if key == curses.KEY_UP:
self.pos = max(self.pos - 1, 0)
elif key == curses.KEY_DOWN:
self.pos = min(self.pos + 1, scroll_limit)
elif key == curses.KEY_PPAGE:
self.pos = max(self.pos - 10, 0)
elif key == curses.KEY_NPAGE:
self.pos = min(self.pos + 10, scroll_limit)
else:
return False
return True
LUT = {
-24: "y",
-21: "z",
-18: "a",
-15: "f",
-12: "p",
-9: "n",
-6: u"\xb5",
-3: "m",
0: "",
3: "k",
6: "M",
9: "G",
12: "T",
15: "P",
18: "E",
21: "Z",
24: "Y"
}
def eng(x, sigfigs=7):
"""Return x in engineering notation"""
if x == 0.0:
return "0"
elif x < 0.0:
return '-'+eng (-x, sigfigs)
m = math.floor(math.log10(x)/3)*3
coeff = "{0:.{1}g}".format (x * 10.0 ** (-m), sigfigs)
lut = LUT.get (m, None)
if lut is None:
return coeff + "e" + str (m)
if lut != '': lut = '_' + lut
#if '.' in coeff and lut != '':
# return coeff.replace ('.', lut)
#else:
# return coeff + lut
return (coeff + lut)
def RPN_drawstack (stack, window):
"""Draw the stack onto a curses window.
"""
for i, item in enumerate (stack):
row = curses.LINES - len (stack) - 2 + i
if row < 1:
continue
tag = None
mode = 0
if isinstance (item, Tagged):
tag = item.tag
item = item.num
if hasattr (item, "magnitude"):
# TODO: HACK
# pint will add a way to format just the abbreviated unit in 0.7.
# For now, I'll format a known magnitude and then remove the known
# magnitude
one_magnitude_unit = format (UREG.Quantity (1, item.units), "~")
assert one_magnitude_unit.startswith ("1 ")
units_abbrev = one_magnitude_unit[2:]
if FORMAT == "eng":
formatted = eng (item.magnitude, SIGFIGS) + " " + units_abbrev
else:
formatted = "%e %s" % (item.magnitude, units_abbrev)
elif isinstance (item, float):
if FORMAT == "eng":
formatted = eng (item, SIGFIGS)
else:
formatted = "%e" % item
elif isinstance (item, str):
formatted = item
mode = curses.color_pair(1)
else:
formatted = repr(item)
if tag is None:
window.addstr (row, 0, "%3d %20s" % (i, formatted), mode)
else:
window.addstr (row, 0, "%3d %20s : %s" % (i, formatted, tag), mode)
curses.doupdate ()
def do_resize (window):
"""Handles redrawing the window if it has been resized.
"""
resize = curses.is_term_resized (curses.LINES, curses.COLS)
if resize:
y, x = window.getmaxyx ()
window.clear ()
curses.resizeterm (y, x)
curses.doupdate ()
################################################################################
# Command definitions
def BINARY (f):
"""Create a binary operator from a lambda function."""
def cmd (stack, args=None):
y = stack.pop ()
x = stack.pop ()
stack.append (f (x, y))
return cmd
def UNARY (f):
"""Create a unary operator from a lambda function.
The unary operators know how to handle tagged numbers - they reapply the
same tag after performing the operation."""
def cmd (stack, args=None):
x = stack.pop ()
if isinstance(x, Tagged):
newtagged = Tagged(f(x.num), x.tag)
stack.append (newtagged)
else:
stack.append (f (x))
return cmd
def DEGREE_TRIG (f):
"""Create a unary trig operator that accepts degrees.
This has to be handled specially because pint supports degrees/radians,
performing the conversion automatically. We have to avoid double-converting,
and also work in the absence of pint.
"""
def g(x):
if hasattr (x, "magnitude"):
x_deg = x.to (UREG.degree)
else:
x_deg = x * math.pi / 180
return f (x_deg)
return UNARY (g)
def DEGREE_INVTRIG (f):
"""Create a unary inverse trig operator that returns degrees.
This returns a pint object if pint is installed.
"""
if pint is not None:
def g(x):
return UREG.Quantity (f (x), UREG.radian).to (UREG.degree)
else:
def g(x):
return f (x) * 180 / math.pi
return UNARY (g)
def _tag (stack, args=None):
"""Pop a string and then a number from the stack, then create a tagged
number."""
tag = stack.pop ()
num = stack.pop ()
if not isinstance(tag, str):
raise TypeError ("to create tagged number, expect Y: number, X: string")
elif not isinstance(num, float) and not hasattr(num, "magnitude"):
raise TypeError ("to create tagged number, expect Y: number, X: string ")
stack.append (Tagged (num, tag))
def _untag (stack, args=None):
"""Pop a tagged number, then push back the original number and tag."""
tagged = stack.pop ()
if not isinstance(tagged, Tagged):
stack.append (tagged)
else:
stack.append (tagged.num)
def _drop (stack, args=None):
stack.pop ()
def _dup (stack, args=None):
if len (stack):
stack.append (stack[-1])
def _undo (stack, args=None):
# Have to run twice, because undopush was called right before this
stack.undo ()
stack.undo ()
def _xchg (stack, args=None):
a = stack.pop ()
b = stack.pop ()
stack.append (a)
stack.append (b)
def _get (stack, args=None):
"""Grab an element from the stack by index and dup it onto the end"""
n = stack.pop ()
if n >= 0 and n < len (stack):
stack.append (stack [int (n)])
else:
stack.append (0.)
def _help (stack, args=None):
"""Display the command helptexts"""
scrollbox = CursesScrollBox (-4, -4)
scrollbox.set_title ("HELP :: up/down/pgup/pgdown; other=close")
lines_left = []
lines_right = []
for key in COMMANDS.keys():
val = COMMANDS[key]
if isinstance(key, int):
lines_left.append (" " * 26)
lines_left.append ("%-26s" % val)
else:
lines_left.append ("%-10s %-15s" % (key, val[1]))
for key in SINGLE_LETTER_COMMANDS.keys():
if not isinstance (key, str):
continue
if key[0] not in string.digits+string.ascii_letters+string.punctuation:
continue
val = SINGLE_LETTER_COMMANDS[key]
lines_right.append ("%-10s %-15s" % (key, val[1]))
if len (lines_left) < len (lines_right):
lines_left.extend ([""] * (len (lines_right) - len (lines_left)))
if len (lines_left) > len (lines_right):
lines_right.extend ([""] * (len (lines_left) - len (lines_right)))
lines = [i + " " + j for i,j in zip(lines_left, lines_right)]
scrollbox.set_text (lines)
scrollbox.show ()
def _exc (stack, args=None):
"""Display the latest exception"""
if ERROR is None:
return
s = traceback.format_tb (ERROR[2])
scrollbox = CursesScrollBox (-4, -4)
scrollbox.set_title ("LAST BACKTRACE")
scrollbox.set_text (s)
scrollbox.show ()
def _eng (stack, args=None):
global FORMAT, SIGFIGS
FORMAT = "eng"
if args is not None and len(args) == 1:
SIGFIGS = int(args[0])
else:
SIGFIGS = 7
def _sci (stack, args=None):
global FORMAT
FORMAT = "sci"
def _copy (stack, args=None):
item = stack[-1]
if isinstance(item, str):
fmt = item
elif isinstance(item, float):
fmt = str(item)
elif isinstance(item, Tagged):
fmt = str(item.num)
if pyperclip is not None:
pyperclip.copy (fmt)
else:
xclip = subprocess.Popen (["xclip", "-selection", "CLIPBOARD"], stdin=subprocess.PIPE)
xclip.stdin.write (fmt.encode('utf8'))
xclip.stdin.close ()
xclip.wait ()
def _paste (stack, args=None):
if pyperclip is not None:
item = float (pyperclip.paste ())
stack.append (item)
else:
clipbd = subprocess.check_output (["xclip", "-selection", "CLIPBOARD", "-o"])
item = float(clipbd)
stack.append (item)
def _convert (stack, args=None):
if args is not None:
units = UREG.parse_units (args)
value = stack.pop ()
else:
units = stack.pop ().units
value = stack.pop ()
if isinstance (value, Tagged):
tag = value.tag
value = value.num
else:
tag = None
converted = value.to (units)
if tag is not None:
converted = Tagged (converted, tag)
stack.append (converted)
def _sh (stack, arg):
newenv = dict (os.environ)
if not stack:
newenv["N"] = "0"
newenv["F"] = "0"
else:
num = stack[-1]
if isinstance(num, Tagged):
num = num.num
if isinstance(num, float):
newenv["N"] = "%g" % num
newenv["F"] = "%g" % num
else:
newenv["N"] = "%g" % num.magnitude
newenv["F"] = "{:g~}".format (num)
try:
p = subprocess.Popen (arg, shell=True, env=newenv, stdout=subprocess.PIPE)
data = p.stdout.read ().decode ("utf8")
p.wait ()
except KeyboardInterrupt:
data = ""
FLOAT_RE = r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?"
match = re.search (FLOAT_RE, data)
if match is not None:
try:
stack.append (float (match.group(0)))
except ValueError as e:
pass
scrollbox = CursesScrollBox (-4, -4)
scrollbox.set_title ("COMMAND OUTPUT")
scrollbox.set_text (data)
scrollbox.show ()
def _preferred (stack, arg):
U = UREG.parse_units
PREFERRED = [
U("V"), U("V/m"),
U("ohm"), U("farad"), U("henry"),
U("W"), U("J"),
U("s"), U("Hz"), U("m"), U("g"),
]
value = stack.pop ()
if isinstance (value, Tagged):
tag = value.tag
value = value.num
else:
tag = None
value_base = value.to_base_units ()
dest_unit = None
for dest in PREFERRED:
src = UREG.Quantity(1., dest).to_base_units().units
if src == value_base.units:
dest_unit = dest
break
if dest_unit is None:
converted = value
else:
converted = value.to (dest_unit)
if tag is not None:
converted = Tagged (converted, tag)
stack.append (converted)
################################################################################
# ADDING SINGLE-LETTER COMMANDS
#
# Insert a compound tuple into the OrderedDict below:
# (key, (operator, helptext))
#
# key: the key returned by curses. For printables, just the character.
# operator: a function that takes the current UndoStack, operates on it,
# and returns nothing. Any exceptions thrown in here will be
# caught and handled, so don't worry about those.
# helptext: a string to display on the help screen.
#
#
# Standard unary and binary operators can be made with UNARY() and BINARY(),
# which accept a lambda function taking one or two arguments and return an
# operator closure around it.
SINGLE_LETTER_COMMANDS = collections.OrderedDict([
("\x7f", (_drop, "drop")),
("\x08", (_drop, "drop")),
(curses.KEY_BACKSPACE, (_drop, "drop")),
("\r", (_dup, "dup")),
("\n", (_dup, "dup")),
(curses.KEY_ENTER, (_dup, "dup")),
("-", (BINARY (lambda x, y: x - y), "subtract")),
("+", (BINARY (lambda x, y: x + y), "add")),
("*", (BINARY (lambda x, y: x * y), "multiply")),
("/", (BINARY (lambda x, y: x / y), "divide")),
("^", (BINARY (lambda x, y: x ** y), "power")),
("e", (UNARY (math.exp), "exponential")),
("r", (UNARY (math.sqrt), "sq root")),
("i", (UNARY (lambda x: 1/x), "reciprocal")),
("u", (_undo, "undo")),
("x", (_xchg, "exchange")),
("[", (_get, "dup item by number")),
("t", (_tag, "attach tag to number")),
("T", (_untag, "unTag")),
("U", (UNARY (lambda x: x.magnitude), "unUnit")),
("c", (_copy, "copy to clipboard")),
("v", (_paste, "paste from clipboard")),
("?", (_help, "display help")),
])
################################################################################
# ADDING NAMED COMMANDS
#
# Insert a compound tuple into the OrderedDict below:
# (name, (operator, helptext))
#
# name: the full command name, including beginning single-quote
# operator: a function that takes the current UndoStack, operates on it,
# and returns nothing. Any exceptions thrown in here will be
# caught and handled, so don't worry about those.
# helptext: a string to display on the help screen.
#
#
# Standard unary and binary operators can be made with UNARY() and BINARY(),
# which accept a lambda function taking one or two arguments and return an
# operator closure around it.
#
#
# You can insert section headers into the help text by adding a compound tuple:
# (id, header)
#
# id: any unique integer
# header: header text
COMMANDS = collections.OrderedDict([
(1, "==TRIGONOMETRY, RAD=="),
("'sin", (UNARY (math.sin), "sin(rad)")),
("'cos", (UNARY (math.cos), "cos(rad)")),
("'tan", (UNARY (math.tan), "tan(rad)")),
("'asin", (UNARY (lambda x: math.asin(x) * Q_("1 rad")), "asin->rad")),
("'acos", (UNARY (lambda x: math.acos(x) * Q_("1 rad")), "acos->rad")),
("'atan", (UNARY (lambda x: math.atan(x) * Q_("1 rad")), "atan->rad")),
(2, "==TRIGONOMETRY, DEG=="),
("'sind", (DEGREE_TRIG (math.sin), "sin(deg)")),
("'cosd", (DEGREE_TRIG (math.cos), "cos(deg)")),
("'tand", (DEGREE_TRIG (math.tan), "tan(deg)")),
("'asind", (DEGREE_INVTRIG (math.asin), "asin->deg")),
("'acosd", (DEGREE_INVTRIG (math.acos), "acos->deg")),
("'atand", (DEGREE_INVTRIG (math.atan), "atan->deg")),
("'deg", (UNARY (math.degrees), "rad->deg")),
("'rad", (UNARY (math.radians), "deg->rad")),
(3, "==HYPERBOLICS=="),
("'sinh", (UNARY (math.sinh), "sinh")),
("'cosh", (UNARY (math.cosh), "cosh")),
("'tanh", (UNARY (math.tanh), "tanh")),
("'asinh", (UNARY (math.asinh), "asinh")),
("'acosh", (UNARY (math.acosh), "acosh")),
("'atanh", (UNARY (math.atanh), "atanh")),
(4, "==LOGARITHMS=="),
("'log", (UNARY (math.log), "log base e")),
("'log10", (UNARY (math.log10), "log base 10")),
("'log2", (UNARY (math.log2), "log base 2")),
(7, "==UNITS=="),
("'mag", (UNARY (lambda x: x.magnitude), "return magnitude without unit")),
("'unity", (UNARY (lambda x: UREG.Quantity(1., x.units)), "return 1.0*unit")),
("'base", (UNARY (lambda x: x.to_base_units()), "convert to base units")),
("'pu", (_preferred, "convert to preferred units")),
("'conv", (_convert, "convert Y to units of X, or X to units of command argument")),
(5, "==CONSTANTS=="),
("'pi", (lambda stack,a=None: stack.append (math.pi), "const PI")),
("'e", (lambda stack,a=None: stack.append (math.e), "const E")),
("'c", (lambda stack,a=None: stack.append (Q_("2.99792458e8 m/s")), "speed of light, m/s")),
("'h", (lambda stack,a=None: stack.append (Q_("6.6260755e-34 J S")), "Planck constant, J s")),
("'k", (lambda stack,a=None: stack.append (Q_("1.380658e-23 J/K")), "Boltzmann constant, J/K")),
("'elec", (lambda stack,a=None: stack.append (Q_("1.60217733e-19 C")), "Charge of electron, C")),
("'e0", (lambda stack,a=None: stack.append (Q_("8.854187817e-12 F/m")), "Permittivity of vacuum, F/m")),
("'amu", (lambda stack,a=None: stack.append (Q_("1.6605402e-27 kg")), "Atomic mass unit, kg")),
("'Na", (lambda stack,a=None: stack.append (Q_("6.0221367e23 1/mol")), "Avogadro's number, mol^-1")),
("'atm", (lambda stack,a=None: stack.append (Q_("101325. Pa")), "Standard atmosphere, Pa")),
(6, "==LERPN=="),
("'sh", (_sh, "run shell command, $N = magnitude, $F = full number with unit")),
("'help", (_help, "display help")),
("'exc", (_exc, "display latest exception backtrace")),
("'eng", (_eng, "engineering mode, sig figs = 7 or command argument")),
("'sci", (_sci, "scientific mode")),
])
def get_single_key_command (key):
"""
Return a single-key command for the key, or None.
"""
if key > 127:
lookup_key = key
else:
lookup_key = chr (key)
return SINGLE_LETTER_COMMANDS.get (lookup_key, None)
def do_command (stack, cmd):
try:
stack.undopush ()
cmd[0] (stack)
except Exception:
stack.undo ()
return sys.exc_info ()
else:
return None
ENTER_KEYS = [curses.KEY_ENTER, 10, 13]
BACKSPACE_KEYS = [curses.KEY_BACKSPACE, 127, 8]
def do_edit (key, strbuf):
"""
Line-edit.
Returns "BACKSPACE", "ENTER", or None
"""
if key in BACKSPACE_KEYS:
if len (strbuf):
del strbuf[-1]
return "BACKSPACE"
if key in ENTER_KEYS:
return "ENTER"
try:
strbuf.append (chr (key))
except ValueError:
pass
return None
def RPN_prompt (stack, error, window):
"""Displays the prompt.
stack: buffer current UndoStack
error: any error text returned from the _previous_ RPN_prompt, or None
window: curses window
returns any error text from the command, or None.
"""
if error is not None:
global ERROR
ERROR = error
window.addstr (0, 0, "ERROR: %s" % str (error[1]))
STATES = [
"FIRSTCHAR", # first character determines the mode
"NUMERIC", # parsing numeric
"NUMUNIT", # parsing unit inside numeric
"COMMAND", # parsing command
"STRING", # parsing string
]
state = "FIRSTCHAR"
strbuf = []
while True:
assert state in STATES
RPN_drawstack (stack, window)
window.move (curses.LINES - 1, 0)
window.addstr ("? ")
for i in strbuf:
window.addstr (i)
window.clrtoeol ()
curses.doupdate ()
try:
key = window.getch ()
except KeyboardInterrupt:
sys.exit (0)
try:
char = chr (key)
except ValueError:
char = None
if state == "FIRSTCHAR":
# Try single-key commands first
cmd = get_single_key_command (key)
if cmd is not None:
return do_command (stack, cmd)
if char is None:
continue
if char in string.digits + "_.":
strbuf.append (char)
state = "NUMERIC"
if char == "'":
strbuf.append (char)
state = "COMMAND"
elif char == '"':
strbuf.append (char)
state = "STRING"
elif state == "NUMERIC":
cmd = get_single_key_command (key)
# Make exception for 'e' so that we can type sci notation
if char == "e" or char == "E" or (("e" in strbuf or "E" in strbuf) and char == "-"):
cmd = None
edit_type = do_edit (key, strbuf)
if edit_type == "ENTER" or (cmd is not None and edit_type != "BACKSPACE"):
if cmd is not None and strbuf and edit_type != "ENTER":
# Do not include command in number
del strbuf[-1]
try:
if strbuf[0] == "_":
strbuf[0] = "-"
f = num_parser (''.join(strbuf))
except Exception:
return sys.exc_info ()
else:
stack.append (f)
if edit_type != "ENTER" and cmd is not None:
return do_command (stack, cmd)
else:
return None
if strbuf and strbuf[-1] == " ":
state = "NUMUNIT"
if not strbuf:
state = "FIRSTCHAR"
if char is None:
continue
elif state == "NUMUNIT":
# Like numeric, but since units can contain letters that have
# "single-unit commands" in them, ignore those.
if do_edit (key, strbuf) == "ENTER":
try:
if strbuf[0] == "_":
strbuf[0] = "-"
f = num_parser (''.join(strbuf))
except Exception:
return sys.exc_info ()
else:
stack.append (f)
return
if not strbuf:
state = "FIRSTCHAR"
elif state == "COMMAND":
if do_edit (key, strbuf) == "ENTER":
command_string = ''.join (strbuf)
cmd_and_args = command_string.partition(" ")
cmd = COMMANDS.get (cmd_and_args[0], None)
if cmd is not None:
try:
stack.undopush ()
cmd[0] (stack, cmd_and_args[2])
except Exception:
stack.undo ()
return sys.exc_info ()
else:
return None
elif not len (strbuf):
return None
else:
try:
raise Exception ("Command not found: %s" % ''.join(strbuf))
except Exception:
return sys.exc_info ()
if not strbuf:
state = "FIRSTCHAR"
elif state == "STRING":
if do_edit (key, strbuf) == "ENTER":
stack.append (''.join (strbuf).lstrip('"'))
return None
if not strbuf:
state = "FIRSTCHAR"
def main (stdscr):
curses.nonl ()
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
error = None
stack = UndoStack ()
while True:
stdscr.clear ()
RPN_drawstack (stack, stdscr)
error = RPN_prompt (stack, error, stdscr)
if __name__ == "__main__":
curses.wrapper (main)

View File

@ -4,8 +4,8 @@ It's a curses RPN calculator written in straight Python. Not actually Linux-depe
despite the (old) name. Should work on both Python 2 and 3, though I only regularly
use/test it in Py3.
Very simple. All one file, no installer - just shove it in your $PATH
somewhere. And don't yell at me when it breaks your computer.
To install, just do `python setup.py install --user`. To test-drive, you should be able
to call `python ./lerpn` from in the root of the source tree.
See the code for documentation.

984
lerpn
View File

@ -1,986 +1,6 @@
#!/usr/bin/python
# Written in 2015 by Christopher Pavlina.
###############################################################################
# lerpn - Linux Engineering RPN calculator
#
# This is a simple RPN calculator using Python and curses. How to use it should
# be obvious for those familiar with RPN. The basic commands:
#
# + - * /
#
# enter dup
# x exchange/swap
# bksp drop
# ? display help
# ctrl-C exit
#
#
# Numbers are displayed in engineering notation with metric prefixes. You
# cannot TYPE them this way, however; that would cause too many collisions with
# single-key commands IMHO.
#
# Not all commands are a single key. Less-used commands begin with a
# single-quote, then the command name, then 'enter'. For example, the sequence
# of keys used to compute the sine of 3.14 would be:
#
# 3 . 1 4 <enter> ' s i n <enter>
#
# You can tag numbers to help remember what they are. To do this, first have
# the number at the end of the stack. Then, push a string to the stack, by
# typing a double-quote ", then the string text, then <enter>. Then, press
# lower-case t to combine the number and the tag.
#
# Basic math can be done to numbers while tagged, but if you perform a binary
# operation on two tagged numbers, lerpn won't know which tag to keep.
#
# To untag a number, use upper-case T to split the number from its tag. You can
# then drop the tag.
#
# This is currently the only application for strings on the stack. Lone strings
# are displayed in green to make them obvious - otherwise, you'll be mighty
# confused when 2 + 3 = 23 !
#
###############################################################################
# ADDING COMMANDS
#
# Grep this source for ADDING SINGLE-LETTER COMMANDS or ADDING NAMED COMMANDS.
#
###############################################################################
#
# This software is free. Do whatever the fuck you want with it. Copy it, change
# it, steal it, strip away this notice, print it and tear it into confetti to
# throw at random dudes on the street. Really, I don't care.
import collections
import curses
import math
import string
import os
import pipes
import re
import shlex
import subprocess
import sys
import traceback
import LerpnApp
# Py2/3 bits
if not hasattr (math, "log2"):
math.log2 = lambda x: math.log(x, 2)
# Hack warning!
# Pint takes too long to load for a general-purpose calculator that has to come
# up quickly. Since it's not needed until after the first line has been
# entered, I'm throwing up a loader in a thread. Blame nickjohnson if it's bad
import threading
pint_load_event = threading.Event()
pint=None
UREG=None
def pintloader():
global pint, _num_parser, UREG, _Q_
try:
import pint
UREG = pint.UnitRegistry ()
UREG.autoconvert_offset_to_baseunit = True
def _num_parser(x):
# Do not return int!
ret = UREG.parse_expression (x)
if isinstance (ret, int):
return float (ret)
else:
return ret
def _Q_(x):
return UREG.parse_expression (x)
pint_load_event.set ()
except ImportError:
pint = None
_num_parser = float
def _Q_(x):
return float (x.partition(" ")[0])
pint_load_event.set ()
def num_parser(x):
pint_load_event.wait()
if '"' in x:
x, delim, tag = x.partition('"')
else:
tag = None
parsed_num = _num_parser(x)
if tag is None:
return parsed_num
else:
return Tagged (parsed_num, tag.strip())
def Q_(*args, **kwargs):
pint_load_event.wait()
return _Q_(*args, **kwargs)
pL = threading.Thread(target=pintloader)
pL.start()
# Pyperclip is optional
try:
import pyperclip
except ImportError:
pyperclip = None
FORMAT="eng"
SIGFIGS=7
ERROR=None
class UndoStack (list):
"""Stack supporting an 'undo' action.
This is used as the main RPN stack. You can append() and pop() and [] just
like with a normal list. You can also use undopush() to push a duplicate of
the stack itself onto a "stack of stacks", and then undo() to restore that.
"""
def __init__ (self, v=None):
"""Initialize an UndoStack from an optional source list.
"""
if v is None:
list.__init__(self)
self.__sos = []
else:
list.__init__(self, v)
self.__sos = []
def undopush (self):
"""Save the current stack state to be restored later with undo()
"""
self.__sos.append (self[:])
def undo (self):
"""Restore the last saved undo state
"""
if len (self.__sos) > 1:
self[:] = self.__sos.pop ()
else:
self[:] = []
class Tagged (object):
"""Tagged number object.
This behaves like a number, but also contains a string tag. The values
are accessible at .num and .tag
"""
def __init__ (self, num, tag):
self.num = num
self.tag = tag
# Add operations to Tagged
ops = [
("add", lambda x,y: x+y),
("sub", lambda x,y: x-y),
("mul", lambda x,y: x*y),
("truediv", lambda x,y: x/y),
("mod", lambda x,y: x%y),
("pow", lambda x,y: x**y),
]
for opname, opfunc in ops:
# For the uninitiated, the default-argument parameters create a new
# scope for the variable, allowing passing each loop iteration's value
# to the closure instead of closing around the single final value.
def method (self, other, opfunc=opfunc):
if isinstance (other, Tagged):
# If both are tagged, just remove the tag.
return opfunc (self.num, other.num)
if not isinstance (other, float): return NotImplemented
return Tagged (opfunc (self.num, other), self.tag)
def rmethod (self, other, opfunc=opfunc):
if isinstance (other, Tagged):
# If both are tagged, just remove the tag.
return opfunc (other.num, self.num)
if not isinstance (other, float): return NotImplemented
return Tagged (opfunc (other, self.num), self.tag)
setattr (Tagged, "__%s__" % opname, method)
setattr (Tagged, "__r%s__" % opname, rmethod)
class CursesScrollBox (object):
"""Displays some text in a scroll box."""
def __init__ (self, width, height):
"""Initialize.
@param width - desired width: positive for absolute width, negative
for margin around window edge
@param height - desired height: positive for absolute height,
negative for margin around window edge
"""
self.w = width
self.h = height
self.title = ""
self.pos = 0
def set_text (self, text):
self.text = text
def set_title (self, title):
self.title = title
def show (self):
if self.w < 0:
self._w = curses.COLS + self.w
else:
self._w = self.w
if self.h < 0:
self._h = curses.LINES + self.h
else:
self._h = self.h
self._x = curses.COLS // 2 - self._w // 2
self._y = curses.LINES // 2 - self._h // 2
curses.curs_set (0)
curses.doupdate ()
self.win = curses.newwin (self._h, self._w, self._y, self._x)
while self._show ():
pass
curses.curs_set (1)
def _show (self):
self.win.clear ()
self.win.move (1, 2)
self.win.addstr (self.title)
self.win.keypad (1)
if isinstance(self.text, str):
lines = self.text.split("\n")
elif isinstance(self.text, list):
lines = self.text
else:
raise TypeError ("Text must be list or str")
view_lines = lines[self.pos:self.pos+self._h - 4]
for y, line in enumerate (view_lines):
self.win.move (y + 3, 2)
self.win.addstr (line.rstrip())
if self.pos > 0:
self.win.move (2, self._w - 3)
self.win.addstr ("+")
if self.pos + self._h < len (lines) + 4:
self.win.move (self._h - 2, self._w - 3)
self.win.addstr ("+")
scroll_limit = len (lines) - self._h // 2
self.win.border ()
try:
key = self.win.getch ()
except KeyboardInterrupt:
return False
if key == curses.KEY_UP:
self.pos = max(self.pos - 1, 0)
elif key == curses.KEY_DOWN:
self.pos = min(self.pos + 1, scroll_limit)
elif key == curses.KEY_PPAGE:
self.pos = max(self.pos - 10, 0)
elif key == curses.KEY_NPAGE:
self.pos = min(self.pos + 10, scroll_limit)
else:
return False
return True
LUT = {
-24: "y",
-21: "z",
-18: "a",
-15: "f",
-12: "p",
-9: "n",
-6: u"\xb5",
-3: "m",
0: "",
3: "k",
6: "M",
9: "G",
12: "T",
15: "P",
18: "E",
21: "Z",
24: "Y"
}
def eng(x, sigfigs=7):
"""Return x in engineering notation"""
if x == 0.0:
return "0"
elif x < 0.0:
return '-'+eng (-x, sigfigs)
m = math.floor(math.log10(x)/3)*3
coeff = "{0:.{1}g}".format (x * 10.0 ** (-m), sigfigs)
lut = LUT.get (m, None)
if lut is None:
return coeff + "e" + str (m)
if lut != '': lut = '_' + lut
#if '.' in coeff and lut != '':
# return coeff.replace ('.', lut)
#else:
# return coeff + lut
return (coeff + lut)
def RPN_drawstack (stack, window):
"""Draw the stack onto a curses window.
"""
for i, item in enumerate (stack):
row = curses.LINES - len (stack) - 2 + i
if row < 1:
continue
tag = None
mode = 0
if isinstance (item, Tagged):
tag = item.tag
item = item.num
if hasattr (item, "magnitude"):
# TODO: HACK
# pint will add a way to format just the abbreviated unit in 0.7.
# For now, I'll format a known magnitude and then remove the known
# magnitude
one_magnitude_unit = format (UREG.Quantity (1, item.units), "~")
assert one_magnitude_unit.startswith ("1 ")
units_abbrev = one_magnitude_unit[2:]
if FORMAT == "eng":
formatted = eng (item.magnitude, SIGFIGS) + " " + units_abbrev
else:
formatted = "%e %s" % (item.magnitude, units_abbrev)
elif isinstance (item, float):
if FORMAT == "eng":
formatted = eng (item, SIGFIGS)
else:
formatted = "%e" % item
elif isinstance (item, str):
formatted = item
mode = curses.color_pair(1)
else:
formatted = repr(item)
if tag is None:
window.addstr (row, 0, "%3d %20s" % (i, formatted), mode)
else:
window.addstr (row, 0, "%3d %20s : %s" % (i, formatted, tag), mode)
curses.doupdate ()
def do_resize (window):
"""Handles redrawing the window if it has been resized.
"""
resize = curses.is_term_resized (curses.LINES, curses.COLS)
if resize:
y, x = window.getmaxyx ()
window.clear ()
curses.resizeterm (y, x)
curses.doupdate ()
################################################################################
# Command definitions
def BINARY (f):
"""Create a binary operator from a lambda function."""
def cmd (stack, args=None):
y = stack.pop ()
x = stack.pop ()
stack.append (f (x, y))
return cmd
def UNARY (f):
"""Create a unary operator from a lambda function.
The unary operators know how to handle tagged numbers - they reapply the
same tag after performing the operation."""
def cmd (stack, args=None):
x = stack.pop ()
if isinstance(x, Tagged):
newtagged = Tagged(f(x.num), x.tag)
stack.append (newtagged)
else:
stack.append (f (x))
return cmd
def DEGREE_TRIG (f):
"""Create a unary trig operator that accepts degrees.
This has to be handled specially because pint supports degrees/radians,
performing the conversion automatically. We have to avoid double-converting,
and also work in the absence of pint.
"""
def g(x):
if hasattr (x, "magnitude"):
x_deg = x.to (UREG.degree)
else:
x_deg = x * math.pi / 180
return f (x_deg)
return UNARY (g)
def DEGREE_INVTRIG (f):
"""Create a unary inverse trig operator that returns degrees.
This returns a pint object if pint is installed.
"""
if pint is not None:
def g(x):
return UREG.Quantity (f (x), UREG.radian).to (UREG.degree)
else:
def g(x):
return f (x) * 180 / math.pi
return UNARY (g)
def _tag (stack, args=None):
"""Pop a string and then a number from the stack, then create a tagged
number."""
tag = stack.pop ()
num = stack.pop ()
if not isinstance(tag, str):
raise TypeError ("to create tagged number, expect Y: number, X: string")
elif not isinstance(num, float) and not hasattr(num, "magnitude"):
raise TypeError ("to create tagged number, expect Y: number, X: string ")
stack.append (Tagged (num, tag))
def _untag (stack, args=None):
"""Pop a tagged number, then push back the original number and tag."""
tagged = stack.pop ()
if not isinstance(tagged, Tagged):
stack.append (tagged)
else:
stack.append (tagged.num)
def _drop (stack, args=None):
stack.pop ()
def _dup (stack, args=None):
if len (stack):
stack.append (stack[-1])
def _undo (stack, args=None):
# Have to run twice, because undopush was called right before this
stack.undo ()
stack.undo ()
def _xchg (stack, args=None):
a = stack.pop ()
b = stack.pop ()
stack.append (a)
stack.append (b)
def _get (stack, args=None):
"""Grab an element from the stack by index and dup it onto the end"""
n = stack.pop ()
if n >= 0 and n < len (stack):
stack.append (stack [int (n)])
else:
stack.append (0.)
def _help (stack, args=None):
"""Display the command helptexts"""
scrollbox = CursesScrollBox (-4, -4)
scrollbox.set_title ("HELP :: up/down/pgup/pgdown; other=close")
lines_left = []
lines_right = []
for key in COMMANDS.keys():
val = COMMANDS[key]
if isinstance(key, int):
lines_left.append (" " * 26)
lines_left.append ("%-26s" % val)
else:
lines_left.append ("%-10s %-15s" % (key, val[1]))
for key in SINGLE_LETTER_COMMANDS.keys():
if not isinstance (key, str):
continue
if key[0] not in string.digits+string.ascii_letters+string.punctuation:
continue
val = SINGLE_LETTER_COMMANDS[key]
lines_right.append ("%-10s %-15s" % (key, val[1]))
if len (lines_left) < len (lines_right):
lines_left.extend ([""] * (len (lines_right) - len (lines_left)))
if len (lines_left) > len (lines_right):
lines_right.extend ([""] * (len (lines_left) - len (lines_right)))
lines = [i + " " + j for i,j in zip(lines_left, lines_right)]
scrollbox.set_text (lines)
scrollbox.show ()
def _exc (stack, args=None):
"""Display the latest exception"""
if ERROR is None:
return
s = traceback.format_tb (ERROR[2])
scrollbox = CursesScrollBox (-4, -4)
scrollbox.set_title ("LAST BACKTRACE")
scrollbox.set_text (s)
scrollbox.show ()
def _eng (stack, args=None):
global FORMAT, SIGFIGS
FORMAT = "eng"
if args is not None and len(args) == 1:
SIGFIGS = int(args[0])
else:
SIGFIGS = 7
def _sci (stack, args=None):
global FORMAT
FORMAT = "sci"
def _copy (stack, args=None):
item = stack[-1]
if isinstance(item, str):
fmt = item
elif isinstance(item, float):
fmt = str(item)
elif isinstance(item, Tagged):
fmt = str(item.num)
if pyperclip is not None:
pyperclip.copy (fmt)
else:
xclip = subprocess.Popen (["xclip", "-selection", "CLIPBOARD"], stdin=subprocess.PIPE)
xclip.stdin.write (fmt.encode('utf8'))
xclip.stdin.close ()
xclip.wait ()
def _paste (stack, args=None):
if pyperclip is not None:
item = float (pyperclip.paste ())
stack.append (item)
else:
clipbd = subprocess.check_output (["xclip", "-selection", "CLIPBOARD", "-o"])
item = float(clipbd)
stack.append (item)
def _convert (stack, args=None):
if args is not None:
units = UREG.parse_units (args)
value = stack.pop ()
else:
units = stack.pop ().units
value = stack.pop ()
if isinstance (value, Tagged):
tag = value.tag
value = value.num
else:
tag = None
converted = value.to (units)
if tag is not None:
converted = Tagged (converted, tag)
stack.append (converted)
def _sh (stack, arg):
newenv = dict (os.environ)
if not stack:
newenv["N"] = "0"
newenv["F"] = "0"
else:
num = stack[-1]
if isinstance(num, Tagged):
num = num.num
if isinstance(num, float):
newenv["N"] = "%g" % num
newenv["F"] = "%g" % num
else:
newenv["N"] = "%g" % num.magnitude
newenv["F"] = "{:g~}".format (num)
try:
p = subprocess.Popen (arg, shell=True, env=newenv, stdout=subprocess.PIPE)
data = p.stdout.read ().decode ("utf8")
p.wait ()
except KeyboardInterrupt:
data = ""
FLOAT_RE = r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?"
match = re.search (FLOAT_RE, data)
if match is not None:
try:
stack.append (float (match.group(0)))
except ValueError as e:
pass
scrollbox = CursesScrollBox (-4, -4)
scrollbox.set_title ("COMMAND OUTPUT")
scrollbox.set_text (data)
scrollbox.show ()
def _preferred (stack, arg):
U = UREG.parse_units
PREFERRED = [
U("V"), U("V/m"),
U("ohm"), U("farad"), U("henry"),
U("W"), U("J"),
U("s"), U("Hz"), U("m"), U("g"),
]
value = stack.pop ()
if isinstance (value, Tagged):
tag = value.tag
value = value.num
else:
tag = None
value_base = value.to_base_units ()
dest_unit = None
for dest in PREFERRED:
src = UREG.Quantity(1., dest).to_base_units().units
if src == value_base.units:
dest_unit = dest
break
if dest_unit is None:
converted = value
else:
converted = value.to (dest_unit)
if tag is not None:
converted = Tagged (converted, tag)
stack.append (converted)
################################################################################
# ADDING SINGLE-LETTER COMMANDS
#
# Insert a compound tuple into the OrderedDict below:
# (key, (operator, helptext))
#
# key: the key returned by curses. For printables, just the character.
# operator: a function that takes the current UndoStack, operates on it,
# and returns nothing. Any exceptions thrown in here will be
# caught and handled, so don't worry about those.
# helptext: a string to display on the help screen.
#
#
# Standard unary and binary operators can be made with UNARY() and BINARY(),
# which accept a lambda function taking one or two arguments and return an
# operator closure around it.
SINGLE_LETTER_COMMANDS = collections.OrderedDict([
("\x7f", (_drop, "drop")),
("\x08", (_drop, "drop")),
(curses.KEY_BACKSPACE, (_drop, "drop")),
("\r", (_dup, "dup")),
("\n", (_dup, "dup")),
(curses.KEY_ENTER, (_dup, "dup")),
("-", (BINARY (lambda x, y: x - y), "subtract")),
("+", (BINARY (lambda x, y: x + y), "add")),
("*", (BINARY (lambda x, y: x * y), "multiply")),
("/", (BINARY (lambda x, y: x / y), "divide")),
("^", (BINARY (lambda x, y: x ** y), "power")),
("e", (UNARY (math.exp), "exponential")),
("r", (UNARY (math.sqrt), "sq root")),
("i", (UNARY (lambda x: 1/x), "reciprocal")),
("u", (_undo, "undo")),
("x", (_xchg, "exchange")),
("[", (_get, "dup item by number")),
("t", (_tag, "attach tag to number")),
("T", (_untag, "unTag")),
("U", (UNARY (lambda x: x.magnitude), "unUnit")),
("c", (_copy, "copy to clipboard")),
("v", (_paste, "paste from clipboard")),
("?", (_help, "display help")),
])
################################################################################
# ADDING NAMED COMMANDS
#
# Insert a compound tuple into the OrderedDict below:
# (name, (operator, helptext))
#
# name: the full command name, including beginning single-quote
# operator: a function that takes the current UndoStack, operates on it,
# and returns nothing. Any exceptions thrown in here will be
# caught and handled, so don't worry about those.
# helptext: a string to display on the help screen.
#
#
# Standard unary and binary operators can be made with UNARY() and BINARY(),
# which accept a lambda function taking one or two arguments and return an
# operator closure around it.
#
#
# You can insert section headers into the help text by adding a compound tuple:
# (id, header)
#
# id: any unique integer
# header: header text
COMMANDS = collections.OrderedDict([
(1, "==TRIGONOMETRY, RAD=="),
("'sin", (UNARY (math.sin), "sin(rad)")),
("'cos", (UNARY (math.cos), "cos(rad)")),
("'tan", (UNARY (math.tan), "tan(rad)")),
("'asin", (UNARY (lambda x: math.asin(x) * Q_("1 rad")), "asin->rad")),
("'acos", (UNARY (lambda x: math.acos(x) * Q_("1 rad")), "acos->rad")),
("'atan", (UNARY (lambda x: math.atan(x) * Q_("1 rad")), "atan->rad")),
(2, "==TRIGONOMETRY, DEG=="),
("'sind", (DEGREE_TRIG (math.sin), "sin(deg)")),
("'cosd", (DEGREE_TRIG (math.cos), "cos(deg)")),
("'tand", (DEGREE_TRIG (math.tan), "tan(deg)")),
("'asind", (DEGREE_INVTRIG (math.asin), "asin->deg")),
("'acosd", (DEGREE_INVTRIG (math.acos), "acos->deg")),
("'atand", (DEGREE_INVTRIG (math.atan), "atan->deg")),
("'deg", (UNARY (math.degrees), "rad->deg")),
("'rad", (UNARY (math.radians), "deg->rad")),
(3, "==HYPERBOLICS=="),
("'sinh", (UNARY (math.sinh), "sinh")),
("'cosh", (UNARY (math.cosh), "cosh")),
("'tanh", (UNARY (math.tanh), "tanh")),
("'asinh", (UNARY (math.asinh), "asinh")),
("'acosh", (UNARY (math.acosh), "acosh")),
("'atanh", (UNARY (math.atanh), "atanh")),
(4, "==LOGARITHMS=="),
("'log", (UNARY (math.log), "log base e")),
("'log10", (UNARY (math.log10), "log base 10")),
("'log2", (UNARY (math.log2), "log base 2")),
(7, "==UNITS=="),
("'mag", (UNARY (lambda x: x.magnitude), "return magnitude without unit")),
("'unity", (UNARY (lambda x: UREG.Quantity(1., x.units)), "return 1.0*unit")),
("'base", (UNARY (lambda x: x.to_base_units()), "convert to base units")),
("'pu", (_preferred, "convert to preferred units")),
("'conv", (_convert, "convert Y to units of X, or X to units of command argument")),
(5, "==CONSTANTS=="),
("'pi", (lambda stack,a=None: stack.append (math.pi), "const PI")),
("'e", (lambda stack,a=None: stack.append (math.e), "const E")),
("'c", (lambda stack,a=None: stack.append (Q_("2.99792458e8 m/s")), "speed of light, m/s")),
("'h", (lambda stack,a=None: stack.append (Q_("6.6260755e-34 J S")), "Planck constant, J s")),
("'k", (lambda stack,a=None: stack.append (Q_("1.380658e-23 J/K")), "Boltzmann constant, J/K")),
("'elec", (lambda stack,a=None: stack.append (Q_("1.60217733e-19 C")), "Charge of electron, C")),
("'e0", (lambda stack,a=None: stack.append (Q_("8.854187817e-12 F/m")), "Permittivity of vacuum, F/m")),
("'amu", (lambda stack,a=None: stack.append (Q_("1.6605402e-27 kg")), "Atomic mass unit, kg")),
("'Na", (lambda stack,a=None: stack.append (Q_("6.0221367e23 1/mol")), "Avogadro's number, mol^-1")),
("'atm", (lambda stack,a=None: stack.append (Q_("101325. Pa")), "Standard atmosphere, Pa")),
(6, "==LERPN=="),
("'sh", (_sh, "run shell command, $N = magnitude, $F = full number with unit")),
("'help", (_help, "display help")),
("'exc", (_exc, "display latest exception backtrace")),
("'eng", (_eng, "engineering mode, sig figs = 7 or command argument")),
("'sci", (_sci, "scientific mode")),
])
def get_single_key_command (key):
"""
Return a single-key command for the key, or None.
"""
if key > 127:
lookup_key = key
else:
lookup_key = chr (key)
return SINGLE_LETTER_COMMANDS.get (lookup_key, None)
def do_command (stack, cmd):
try:
stack.undopush ()
cmd[0] (stack)
except Exception:
stack.undo ()
return sys.exc_info ()
else:
return None
ENTER_KEYS = [curses.KEY_ENTER, 10, 13]
BACKSPACE_KEYS = [curses.KEY_BACKSPACE, 127, 8]
def do_edit (key, strbuf):
"""
Line-edit.
Returns "BACKSPACE", "ENTER", or None
"""
if key in BACKSPACE_KEYS:
if len (strbuf):
del strbuf[-1]
return "BACKSPACE"
if key in ENTER_KEYS:
return "ENTER"
try:
strbuf.append (chr (key))
except ValueError:
pass
return None
def RPN_prompt (stack, error, window):
"""Displays the prompt.
stack: buffer current UndoStack
error: any error text returned from the _previous_ RPN_prompt, or None
window: curses window
returns any error text from the command, or None.
"""
if error is not None:
global ERROR
ERROR = error
window.addstr (0, 0, "ERROR: %s" % str (error[1]))
STATES = [
"FIRSTCHAR", # first character determines the mode
"NUMERIC", # parsing numeric
"NUMUNIT", # parsing unit inside numeric
"COMMAND", # parsing command
"STRING", # parsing string
]
state = "FIRSTCHAR"
strbuf = []
while True:
assert state in STATES
RPN_drawstack (stack, window)
window.move (curses.LINES - 1, 0)
window.addstr ("? ")
for i in strbuf:
window.addstr (i)
window.clrtoeol ()
curses.doupdate ()
try:
key = window.getch ()
except KeyboardInterrupt:
sys.exit (0)
try:
char = chr (key)
except ValueError:
char = None
if state == "FIRSTCHAR":
# Try single-key commands first
cmd = get_single_key_command (key)
if cmd is not None:
return do_command (stack, cmd)
if char is None:
continue
if char in string.digits + "_.":
strbuf.append (char)
state = "NUMERIC"
if char == "'":
strbuf.append (char)
state = "COMMAND"
elif char == '"':
strbuf.append (char)
state = "STRING"
elif state == "NUMERIC":
cmd = get_single_key_command (key)
# Make exception for 'e' so that we can type sci notation
if char == "e" or char == "E" or (("e" in strbuf or "E" in strbuf) and char == "-"):
cmd = None
edit_type = do_edit (key, strbuf)
if edit_type == "ENTER" or (cmd is not None and edit_type != "BACKSPACE"):
if cmd is not None and strbuf and edit_type != "ENTER":
# Do not include command in number
del strbuf[-1]
try:
if strbuf[0] == "_":
strbuf[0] = "-"
f = num_parser (''.join(strbuf))
except Exception:
return sys.exc_info ()
else:
stack.append (f)
if edit_type != "ENTER" and cmd is not None:
return do_command (stack, cmd)
else:
return None
if strbuf and strbuf[-1] == " ":
state = "NUMUNIT"
if not strbuf:
state = "FIRSTCHAR"
if char is None:
continue
elif state == "NUMUNIT":
# Like numeric, but since units can contain letters that have
# "single-unit commands" in them, ignore those.
if do_edit (key, strbuf) == "ENTER":
try:
if strbuf[0] == "_":
strbuf[0] = "-"
f = num_parser (''.join(strbuf))
except Exception:
return sys.exc_info ()
else:
stack.append (f)
return
if not strbuf:
state = "FIRSTCHAR"
elif state == "COMMAND":
if do_edit (key, strbuf) == "ENTER":
command_string = ''.join (strbuf)
cmd_and_args = command_string.partition(" ")
cmd = COMMANDS.get (cmd_and_args[0], None)
if cmd is not None:
try:
stack.undopush ()
cmd[0] (stack, cmd_and_args[2])
except Exception:
stack.undo ()
return sys.exc_info ()
else:
return None
elif not len (strbuf):
return None
else:
try:
raise Exception ("Command not found: %s" % ''.join(strbuf))
except Exception:
return sys.exc_info ()
if not strbuf:
state = "FIRSTCHAR"
elif state == "STRING":
if do_edit (key, strbuf) == "ENTER":
stack.append (''.join (strbuf).lstrip('"'))
return None
if not strbuf:
state = "FIRSTCHAR"
def main (stdscr):
curses.nonl ()
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
error = None
stack = UndoStack ()
while True:
stdscr.clear ()
RPN_drawstack (stack, stdscr)
error = RPN_prompt (stack, error, stdscr)
if __name__ == "__main__":
curses.wrapper (main)
curses.wrapper (LerpnApp.main)

13
setup.py Normal file
View File

@ -0,0 +1,13 @@
from setuptools import setup
setup( name='lerpn',
version='0.1',
description='Linux? Engineering RPN calculator',
url='https://github.com/cpavlina/lerpn',
author='Chris Pavlina',
author_email='pavlina.chris@gmail.com',
license='any',
packages=['LerpnApp'],
scripts=['lerpn'],
zip_safe=False)