2"""Calculate mathematical expressions using :py:obj:`ast.parse` (mode="eval")."""
4from __future__
import annotations
15from flask_babel
import gettext
20if typing.TYPE_CHECKING:
27 """Plugin converts strings to different hash digests. The results are
28 displayed in area for the "answers".
33 def __init__(self, plg_cfg:
"PluginCfg") ->
None:
38 name=gettext(
"Basic Calculator"),
39 description=gettext(
"Calculate mathematical expressions via the search bar"),
40 preference_section=
"general",
45 p = mp_fork.Process(target=handler, args=(que, func, args), kwargs=kwargs)
47 p.join(timeout=timeout)
53 self.
log.debug(
"terminate function (%s: %s // %s) after timeout is exceeded", func.__name__, args, kwargs)
59 def post_search(self, request:
"SXNG_Request", search:
"SearchWithPlugins") -> EngineResults:
63 if search.search_query.pageno > 1:
66 query = search.search_query.query
72 query = query.replace(
"x",
"*").replace(
":",
"/")
75 ui_locale = babel.Locale.parse(request.preferences.get_value(
"locale"), sep=
"-")
78 def _decimal(match: re.Match) -> str:
79 val = match.string[match.start() : match.end()]
80 val = babel.numbers.parse_decimal(val, ui_locale, numbering_system=
"latn")
83 decimal = ui_locale.number_symbols[
"latn"][
"decimal"]
84 group = ui_locale.number_symbols[
"latn"][
"group"]
85 query = re.sub(f
"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query)
88 query_py_formatted = query.replace(
"^",
"**")
91 res = self.
timeout_func(0.05, _eval_expr, query_py_formatted)
92 if res
is None or res[0] ==
"":
97 res =
"True" if res != 0
else "False"
99 res = babel.numbers.format_decimal(res, locale=ui_locale)
100 results.add(results.types.Answer(answer=f
"{search.search_query.query} = {res}"))
105def _compare(ops: list[ast.cmpop], values: list[int | float]) -> int:
107 2 < 3 becomes ops=[ast.Lt] and values=[2,3]
108 2 < 3 <= 4 becomes ops=[ast.Lt, ast.LtE] and values=[2,3, 4]
110 for op, a, b
in zip(ops, values, values[1:]):
111 if isinstance(op, ast.Eq)
and a == b:
113 if isinstance(op, ast.NotEq)
and a != b:
115 if isinstance(op, ast.Lt)
and a < b:
117 if isinstance(op, ast.LtE)
and a <= b:
119 if isinstance(op, ast.Gt)
and a > b:
121 if isinstance(op, ast.GtE)
and a >= b:
136operators: dict[type, typing.Callable] = {
137 ast.Add: operator.add,
138 ast.Sub: operator.sub,
139 ast.Mult: operator.mul,
140 ast.Div: operator.truediv,
141 ast.Pow: operator.pow,
142 ast.BitXor: operator.xor,
143 ast.BitOr: operator.or_,
144 ast.BitAnd: operator.and_,
145 ast.USub: operator.neg,
146 ast.RShift: operator.rshift,
147 ast.LShift: operator.lshift,
148 ast.Mod: operator.mod,
149 ast.Compare: _compare,
163mp_fork = multiprocessing.get_context(
"fork")
168 Evaluates the given textual expression.
170 Returns a tuple of (numericResult, isBooleanResult).
172 >>> _eval_expr('2^6')
174 >>> _eval_expr('2**6')
176 >>> _eval_expr('1 + 2*3**(4^5) / (6 + -7)')
178 >>> _eval_expr('1 < 3')
180 >>> _eval_expr('5 < 3')
182 >>> _eval_expr('17 == 11+1+5 == 7+5+5')
186 root_expr = ast.parse(expr, mode=
'eval').body
187 return _eval(root_expr), isinstance(root_expr, ast.Compare)
189 except (SyntaxError, TypeError, ZeroDivisionError):
195 if isinstance(node, ast.Constant)
and isinstance(node.value, (int, float)):
198 if isinstance(node, ast.BinOp):
199 return operators[type(node.op)](
_eval(node.left),
_eval(node.right))
201 if isinstance(node, ast.UnaryOp):
202 return operators[type(node.op)](
_eval(node.operand))
204 if isinstance(node, ast.Compare):
207 if isinstance(node, ast.Name)
and node.id
in math_constants:
208 return math_constants[node.id]
210 raise TypeError(node)
213def handler(q: multiprocessing.Queue, func, args, **kwargs):
215 q.put(func(*args, **kwargs))
None __init__(self, "PluginCfg" plg_cfg)
EngineResults post_search(self, "SXNG_Request" request, "SearchWithPlugins" search)
timeout_func(self, timeout, func, *args, **kwargs)
int _compare(list[ast.cmpop] ops, list[int|float] values)
handler(multiprocessing.Queue q, func, args, **kwargs)