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