# # calculator.py : A calculator module for the deskbar applet. # # Copyright (C) 2006 by Callum McKenzie # # 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # Authors: Callum McKenzie # # Enable this to see what goes wrong when evaluating the string. # Since half-written strings get evaluated, this produces a lot # of crap as well as the answer you want. _debug = False from gettext import gettext as _ from __future__ import division import deskbar.Handler, deskbar.Match import deskbar.Categories import cgi import gtk import re import math if _debug: import traceback # This is evil since I strongly suspect this isn't meant to be public API. # However, I can't see a better way. deskbar.Categories.CATEGORIES["calculator"] = { "name" : _("Calculator") } HANDLERS = { "CalculatorHandler" : { "name" : _("Calculator"), "description" : _("Calculate simple equations"), "version" : "1.5", } } def bin (n): """A local binary equivalent of the hex and oct builtins.""" if (n == 0): return "0b0" s = "" if (n < 0): while n != -1: s = str (n & 1) + s n >>= 1 return "0b" + "...111" + s else: while n != 0: s = str (n & 1) + s n >>= 1 return "0b" + s # These next three make sure {hex, oct, bin} can handle floating point, # by rounding. This makes sure things like hex(255/2) behave as a # programmer would expect while allowing 255/2 to equal 127.5 for normal # people. Abstracting out the body of these into a single function which # takes hex, oct or bin as an argument seems to run into problems with # those functions not being defined correctly in the resticted eval (?). def lenient_hex (c): try: return hex (c) except TypeError: return hex (int (c)) def lenient_oct (c): try: return oct (c) except TypeError: return oct (int (c)) def lenient_bin (c): try: return bin (c) except TypeError: return bin (int (c)) class CalculatorMatch (deskbar.Match.Match): def get_name (self, text=None): # I really think that deskbar should be doing the # escaping for us. escapedtext = cgi.escape (text) return { "name" : self.name, "escapedtext" : escapedtext } def get_verb (self): """We print out the entire equation.""" return "%(escapedtext)s = %(name)s" def get_category (self): return "calculator" def action (self, text=None): for clipboard in ('CLIPBOARD', 'PRIMARY'): gtk.clipboard_get(clipboard).set_text(str(self.name)) class CalculatorHandler (deskbar.Handler.Handler): def __init__ (self): deskbar.Handler.Handler.__init__ (self, "gtk-add") self.hexre = re.compile ("0[Xx][0-9a-fA-F_]*[0-9a-fA-F]") self.binre = re.compile ("0[bB][01_]*[01]") def _number_parser (self, match, base): """A generic number parser, regardless of base. It also ignores the '_' character so it can be used as a separator. Note how we skip the first two characters since we assume it is something like '0x' or '0b' and identifies the base.""" table = { '0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4, '5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9, 'a' : 10, 'b' : 11, 'c' : 12, 'd' : 13, 'e' : 14, 'f' : 15 } d = 0 for c in match.group()[2:]: if c != "_": d = d*base + table[c] return str(d) def _binsub (self, match): """Because python doesn't handle binary literals, we parse it ourselves and replace it with a decimal representation.""" return self._number_parser (match, 2) def _hexsub (self, match): """Parse the hex literal ourselves. We could let python do it, but since we have a generic parser we use that instead.""" return self._number_parser (match, 16) def query (self, query): """We evaluate the equation by first replacing hex and binary literals with their decimal representation. (We need to check hex, so we can distinguish 0x10b1 as a hex number, not 0x1 followed by 0b1.) We severely restrict the eval environment. Any errors are ignored.""" restricted_dictionary = { "__builtins__" : None, "abs" : abs, "acos" : math.acos, "asin" : math.asin, "atan" : math.atan, "atan2" : math.atan2, "bin" : lenient_bin,"ceil" : math.ceil, "cos" : math.cos, "cosh" : math.cosh, "degrees" : math.degrees, "exp" : math.exp, "floor" : math.floor, "hex" : lenient_hex, "int" : int, "log" : math.log, "log10" : math.log10, "oct" : lenient_oct, "pi" : math.pi, "radians" : math.radians, "round": round, "sin" : math.sin, "sinh" : math.sinh, "sqrt" : math.sqrt, "tan" : math.tan, "tanh" : math.tanh} try: scrubbedquery = query.lower() scrubbedquery = self.hexre.sub (self._hexsub, scrubbedquery) scrubbedquery = self.binre.sub (self._binsub, scrubbedquery) for (c1, c2) in (("[", "("), ("{", "("), ("]", ")"), ("}", ")")): scrubbedquery = scrubbedquery.replace (c1, c2) answer = eval (scrubbedquery, restricted_dictionary) # Try and avoid echoing back simple numbers. Note that this # doesn't work well for floating point, e.g. '3.' behaves badly. if str (answer) == query: return [] # We need this check because the eval can return function objects # when we are halfway through typing the expression. if isinstance (answer, (float, int, long, str)): return [ CalculatorMatch (self, name=answer) ] else: return [] except: if _debug: traceback.print_exc () return []