calculator.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. """Calculate mathematical expressions using ack#eval
  3. """
  4. import ast
  5. import re
  6. import operator
  7. from multiprocessing import Process, Queue
  8. from typing import Callable
  9. import flask
  10. import babel
  11. from flask_babel import gettext
  12. from searx.plugins import logger
  13. name = "Basic Calculator"
  14. description = gettext("Calculate mathematical expressions via the search bar")
  15. default_on = True
  16. preference_section = 'general'
  17. plugin_id = 'calculator'
  18. logger = logger.getChild(plugin_id)
  19. operators: dict[type, Callable] = {
  20. ast.Add: operator.add,
  21. ast.Sub: operator.sub,
  22. ast.Mult: operator.mul,
  23. ast.Div: operator.truediv,
  24. ast.Pow: operator.pow,
  25. ast.BitXor: operator.xor,
  26. ast.USub: operator.neg,
  27. }
  28. def _eval_expr(expr):
  29. """
  30. >>> _eval_expr('2^6')
  31. 4
  32. >>> _eval_expr('2**6')
  33. 64
  34. >>> _eval_expr('1 + 2*3**(4^5) / (6 + -7)')
  35. -5.0
  36. """
  37. try:
  38. return _eval(ast.parse(expr, mode='eval').body)
  39. except ZeroDivisionError:
  40. # This is undefined
  41. return ""
  42. def _eval(node):
  43. if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
  44. return node.value
  45. if isinstance(node, ast.BinOp):
  46. return operators[type(node.op)](_eval(node.left), _eval(node.right))
  47. if isinstance(node, ast.UnaryOp):
  48. return operators[type(node.op)](_eval(node.operand))
  49. raise TypeError(node)
  50. def timeout_func(timeout, func, *args, **kwargs):
  51. def handler(q: Queue, func, args, **kwargs): # pylint:disable=invalid-name
  52. try:
  53. q.put(func(*args, **kwargs))
  54. except:
  55. q.put(None)
  56. raise
  57. que = Queue()
  58. p = Process(target=handler, args=(que, func, args), kwargs=kwargs)
  59. p.start()
  60. p.join(timeout=timeout)
  61. ret_val = None
  62. if not p.is_alive():
  63. ret_val = que.get()
  64. else:
  65. logger.debug("terminate function after timeout is exceeded")
  66. p.terminate()
  67. p.join()
  68. p.close()
  69. return ret_val
  70. def post_search(_request, search):
  71. # only show the result of the expression on the first page
  72. if search.search_query.pageno > 1:
  73. return True
  74. query = search.search_query.query
  75. # in order to avoid DoS attacks with long expressions, ignore long expressions
  76. if len(query) > 100:
  77. return True
  78. # replace commonly used math operators with their proper Python operator
  79. query = query.replace("x", "*").replace(":", "/")
  80. # use UI language
  81. ui_locale = babel.Locale.parse(flask.request.preferences.get_value('locale'), sep='-')
  82. # parse the number system in a localized way
  83. def _decimal(match: re.Match) -> str:
  84. val = match.string[match.start() : match.end()]
  85. val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn")
  86. return str(val)
  87. decimal = ui_locale.number_symbols["latn"]["decimal"]
  88. group = ui_locale.number_symbols["latn"]["group"]
  89. query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query)
  90. # only numbers and math operators are accepted
  91. if any(str.isalpha(c) for c in query):
  92. return True
  93. # in python, powers are calculated via **
  94. query_py_formatted = query.replace("^", "**")
  95. # Prevent the runtime from being longer than 50 ms
  96. result = timeout_func(0.05, _eval_expr, query_py_formatted)
  97. if result is None or result == "":
  98. return True
  99. result = babel.numbers.format_decimal(result, locale=ui_locale)
  100. search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"}
  101. return True