From deeaf9eccab80aaee830bb03245a967b94b55768 Mon Sep 17 00:00:00 2001 From: iou1name Date: Fri, 31 Jan 2020 09:47:46 -0500 Subject: [PATCH] overhauled calc module --- README.md | 2 +- modules/calc.py | 39 ++++++--- tools/calculation.py | 197 ------------------------------------------- 3 files changed, 27 insertions(+), 211 deletions(-) delete mode 100755 tools/calculation.py diff --git a/README.md b/README.md index 9f3b47b..51fa48f 100755 --- a/README.md +++ b/README.md @@ -3,4 +3,4 @@ It's like Sopel, except rewritten from scratch using Twisted as a base and over ## Requirements Python 3.6+ -Python packages: `twisted python-dateutil requests bs4 wolframalpha emoji pillow ipython numpy numpngw` +Python packages: `twisted python-dateutil requests bs4 wolframalpha emoji pillow ipython numpy numpngw numexpr` diff --git a/modules/calc.py b/modules/calc.py index b575797..0e3c365 100755 --- a/modules/calc.py +++ b/modules/calc.py @@ -2,30 +2,44 @@ """ A basic calculator and python interpreter application. """ -import sys +import multiprocessing +import numpy +import numexpr import requests from module import commands, example -from tools.calculation import eval_equation BASE_TUMBOLIA_URI = 'https://tumbolia-two.appspot.com/' @commands('calc', 'c') @example('.c 5 + 3', '8') def c(bot, trigger): - """Evaluate some calculation.""" + """Evaluates a mathematical expression.""" if len(trigger.args) < 2: return bot.reply("Nothing to calculate.") - eqn = ' '.join(trigger.args[1:]) - try: - result = eval_equation(eqn) - result = "{:.10g}".format(result) - except ZeroDivisionError: - result = "Division by zero is not supported in this universe." - except Exception as e: - result = "{error}: {msg}".format(error=type(e), msg=e) - bot.reply(result) + expr = ' '.join(trigger.args[1:]) + + def worker(expr, return_dict): + with numpy.errstate(all='ignore'): + try: + value = numexpr.evaluate(expr, local_dict={}, global_dict={}) + value = value.item() + except: + value = "Error. The expression was not understood." + return_dict['value'] = value + + with multiprocessing.Manager() as manager: + d = manager.dict() + proc = multiprocessing.Process(target=worker, args=(expr, d)) + proc.start() + proc.join(1) + if proc.is_alive(): + proc.kill() + proc.join() + proc.close() + return bot.reply("Calculation timed out.") + bot.reply(str(d['value'])) @commands('py') @@ -42,7 +56,6 @@ def py(bot, trigger): answer = res.text answer = answer[:2000] if answer: - #bot.msg can potentially lead to 3rd party commands triggering. bot.msg(answer, length=300) else: bot.reply('Sorry, no result.') diff --git a/tools/calculation.py b/tools/calculation.py deleted file mode 100755 index 72e5b60..0000000 --- a/tools/calculation.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -""" -Tools to help safely do calculations from user input. -""" -import time -import numbers -import operator -import ast - -__all__ = ['eval_equation'] - - -class ExpressionEvaluator: - """ - A generic class for evaluating limited forms of Python expressions. - - Instances can overwrite binary_ops and unary_ops attributes with dicts of - the form {ast.Node, function}. When the ast.Node being used as key is - found, it will be evaluated using the given function. - """ - - class Error(Exception): - pass - - def __init__(self, bin_ops=None, unary_ops=None): - self.binary_ops = bin_ops or {} - self.unary_ops = unary_ops or {} - - def __call__(self, expression_str, timeout=5.0): - """ - Evaluate a python expression and return the result. - - Raises: - SyntaxError: If the given expression_str is not a valid python - statement. - ExpressionEvaluator.Error: If the instance of ExpressionEvaluator - does not have a handler for the ast.Node. - """ - ast_expression = ast.parse(expression_str, mode='eval') - return self._eval_node(ast_expression.body, time.time() + timeout) - - def _eval_node(self, node, timeout): - """ - Recursively evaluate the given ast.Node. - - Uses self.binary_ops and self.unary_ops for the implementation. - - A subclass could overwrite this to handle more nodes, calling it only - for nodes it does not implement it self. - - Raises: - ExpressionEvaluator.Error: If it can't handle the ast.Node. - """ - if isinstance(node, ast.Num): - return node.n - - elif (isinstance(node, ast.BinOp) and - type(node.op) in self.binary_ops): - left = self._eval_node(node.left, timeout) - right = self._eval_node(node.right, timeout) - if time.time() > timeout: - raise ExpressionEvaluator.Error( - "Time for evaluating expression ran out.") - return self.binary_ops[type(node.op)](left, right) - - elif (isinstance(node, ast.UnaryOp) and - type(node.op) in self.unary_ops): - operand = self._eval_node(node.operand, timeout) - if time.time() > timeout: - raise ExpressionEvaluator.Error( - "Time for evaluating expression ran out.") - return self.unary_ops[type(node.op)](operand) - - raise ExpressionEvaluator.Error( - "Ast.Node '%s' not implemented." % (type(node).__name__,)) - - -def guarded_mul(left, right): - """Decorate a function to raise an error for values > limit.""" - # Only handle ints because floats will overflow anyway. - if not isinstance(left, numbers.Integral): - pass - elif not isinstance(right, numbers.Integral): - pass - elif left in (0, 1) or right in (0, 1): - # Ignore trivial cases. - pass - elif left.bit_length() + right.bit_length() > 664386: - # 664386 is the number of bits (10**100000)**2 has, which is instant on - # my laptop, while (10**1000000)**2 has a noticeable delay. It could - # certainly be improved. - raise ValueError( - "Value is too large to be handled in limited time and memory.") - - return operator.mul(left, right) - - -def pow_complexity(num, exp): - """ - Estimate the worst case time pow(num, exp) takes to calculate. - - This function is based on experimetal data from the time it takes to - calculate "num**exp" on laptop with i7-2670QM processor on a 32 bit - CPython 2.7.6 interpreter on Windows. - - It tries to implement this surface: x=exp, y=num - 1e5 2e5 3e5 4e5 5e5 6e5 7e5 8e5 9e5 - e1 0.03 0.09 0.16 0.25 0.35 0.46 0.60 0.73 0.88 - e2 0.08 0.24 0.46 0.73 1.03 1.40 1.80 2.21 2.63 - e3 0.15 0.46 0.87 1.39 1.99 2.63 3.35 4.18 5.15 - e4 0.24 0.73 1.39 2.20 3.11 4.18 5.39 6.59 7.88 - e5 0.34 1.03 2.00 3.12 4.48 5.97 7.56 9.37 11.34 - e6 0.46 1.39 2.62 4.16 5.97 7.86 10.09 12.56 15.39 - e7 0.60 1.79 3.34 5.39 7.60 10.16 13.00 16.23 19.44 - e8 0.73 2.20 4.18 6.60 9.37 12.60 16.26 19.83 23.70 - e9 0.87 2.62 5.15 7.93 11.34 15.44 19.40 23.66 28.58 - - For powers of 2 it tries to implement this surface: - 1e7 2e7 3e7 4e7 5e7 6e7 7e7 8e7 9e7 - 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 - 2 0.21 0.44 0.71 0.92 1.20 1.49 1.66 1.95 2.23 - 4 0.43 0.91 1.49 1.96 2.50 3.13 3.54 4.10 4.77 - 8 0.70 1.50 2.24 3.16 3.83 4.66 5.58 6.56 7.67 - - The function number were selected by starting with the theoretical - complexity of exp * log2(num)**2 and fiddling with the exponents - untill it more or less matched with the table. - - Because this function is based on a limited set of data it might - not give accurate results outside these boundaries. The results - derived from large num and exp were quite accurate for small num - and very large exp though, except when num was a power of 2. - """ - if num in (0, 1) or exp in (0, 1): - return 0 - elif (num & (num - 1)) == 0: - # For powers of 2 the scaling is a bit different. - return exp ** 1.092 * num.bit_length() ** 1.65 / 623212911.121 - else: - return exp ** 1.590 * num.bit_length() ** 1.73 / 36864057619.3 - - -def guarded_pow(left, right): - # Only handle ints because floats will overflow anyway. - if not isinstance(left, numbers.Integral): - pass - elif not isinstance(right, numbers.Integral): - pass - elif pow_complexity(left, right) < 0.5: - # Value 0.5 is arbitary and based on a estimated runtime of 0.5s - # on a fairly decent laptop processor. - pass - else: - raise ValueError("Pow expression too complex to calculate.") - - return operator.pow(left, right) - - -class EquationEvaluator(ExpressionEvaluator): - __bin_ops = { - ast.Add: operator.add, - ast.Sub: operator.sub, - ast.Mult: guarded_mul, - ast.Div: operator.truediv, - ast.Pow: guarded_pow, - ast.Mod: operator.mod, - ast.FloorDiv: operator.floordiv, - ast.BitXor: guarded_pow - } - __unary_ops = { - ast.USub: operator.neg, - ast.UAdd: operator.pos, - } - - def __init__(self): - ExpressionEvaluator.__init__( - self, - bin_ops=self.__bin_ops, - unary_ops=self.__unary_ops - ) - - def __call__(self, expression_str): - result = ExpressionEvaluator.__call__(self, expression_str) - - # This wrapper is here so additional sanity checks could be done - # on the result of the eval, but currently none are done. - - return result - - -eval_equation = EquationEvaluator() -""" -Evaluates a Python equation expression and returns the result. - -Supports addition (+), subtraction (-), multiplication (*), division (/), -power (**) and modulo (%). -"""