I'm writing a Discord bot that will accept user input as a string and then evaluate the expression inside it, nothing fancy just simple arithmetic operations. I have two concerns - safety and decimals. First I used simpleeval package, it worked fine but it had trouble with decimals, e.g
0.1 + 0.1 + 0.1 - 0.3
would return 5.551115123125783e-17
. After googling a lot I found an answer that work's but it uses the built in eval() function and apparently using it is a big no.
Is there a better/safer way of handling this? This implements https://docs.python.org/3/library/tokenize.html#examples decistmt() method which substitutes Decimals for floats in a string of statements. But in the end I use eval() and with all those checks I'm still unsure if it's safe and I'd rather avoid it.
This is what decistmt() does:
from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP
from io import BytesIO
def decistmt(s):
"""Substitute Decimals for floats in a string of statements.
>>> from decimal import Decimal
>>> s = 'print(+21.3e-5*-.1234/81.7)'
>>> decistmt(s)
"print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"
The format of the exponent is inherited from the platform C library.
Known cases are "e-007" (Windows) and "e-07" (not Windows). Since
we're only showing 12 digits, and the 13th isn't close to 5, the
rest of the output should be platform-independent.
>>> exec(s) #doctest: +ELLIPSIS
-3.21716034272e-0...7
Output from calculations with Decimal should be identical across all
platforms.
>>> exec(decistmt(s))
-3.217160342717258261933904529E-7
"""
result = []
g = tokenize(BytesIO(s.encode('utf-8')).readline) # tokenize the string
for toknum, tokval, _, _, _ in g:
if toknum == NUMBER and '.' in tokval: # replace NUMBER tokens
result.extend([
(NAME, 'Decimal'),
(OP, '('),
(STRING, repr(tokval)),
(OP, ')')
])
else:
result.append((toknum, tokval))
return untokenize(result).decode('utf-8')
# example user input: "(20+5)^4 - 550 + 8"
@bot.command()
async def calc(context, *, user_input):
#this is so user can use both ^ and ** for power and use "," and "." for decimals
equation = user_input.replace('^', "**").replace(",", ".")
valid_operators: list = ["+", "-", "/", "*", "%", "^", "**"]
# checks if a string contains any element from a list, it will also return false if the iterable is empty, so this covers empty check too
operator_check: bool = any(
operator in equation for operator in valid_operators)
# checks if arithmetic operator is last or first element in equation, to prevent doing something like ".calc 2+" or ".calc +2"
def is_last_or_first(equation: str):
for operator in valid_operators:
if operator == equation[-1]:
return True
elif operator == equation[0]:
if operator == "-":
return False
else:
return True
#isupper and islower checks whether there are letters in user input
if not operator_check or is_last_or_first(equation) or equation.isupper() or equation.islower():
return await context.send("Invalid input")
result = eval(decistmt(equation))
result = float(result)
# returning user_input here so user sees "^" instead of "**"
async def result_generator(result: int or float):
await context.send(f'**Input:** ```fix\n{user_input}```**Result:** ```fix\n{result}```')
# this is so if the result is .0 it returns an int
if result.is_integer():
await result_generator(int(result))
else:
await result_generator(result)
This is what happens after user input
user_input = "0.1 + 0.1 + 0.1 - 0.3"
float_to_decimal = decistmt(user_input)
print(float_to_decimal)
print(type(float_to_decimal))
# Decimal ('0.1')+Decimal ('0.1')+Decimal ('0.1')-Decimal ('0.3')
# <class 'str'>
Now I need to evaluate this input so I'm using eval(). My question is - is this safe (I assume not) and is there some other way to evaluate float_to_decimal?
EDIT. As requested, more in depth explanation:
The whole application is a chat bot. This "calc" function is one of the commands users can use. It is invoked by inputing ".calc " in the chat, ".calc" is a prefix, anything after that is arguments and it will be concatenated to a string and ultimately a string is what I'll get as an argument. I perform a bunch of checks to limit the input (remove letters etc.). After checks I am left with a string consisting of numbers, arithmetic operators and brackets. I want to evaluate the mathematical expression from that string. I pass that string to decistmt function which transforms each float in that string to Decimal objects, the result IS A STRING looking like this: "Decimal ('2.5') + Decimal ('-5.2')". Now I need to evaluate the expression inside that string. I used simpleeval module for that but it is incompatible with Decimal module so I'm evaluating using built in eval() method. My question is, is there a safer way of evaluating mathematical expression in a string like that?