2"""Calculate mathematical expressions using :py:obj:`ast.parse` (mode="eval")."""
14from flask_babel
import gettext
19if typing.TYPE_CHECKING:
26 """Plugin converts strings to different hash digests. The results are
27 displayed in area for the "answers".
32 def __init__(self, plg_cfg:
"PluginCfg") ->
None:
37 name=gettext(
"Basic Calculator"),
38 description=gettext(
"Calculate mathematical expressions via the search bar"),
39 preference_section=
"general",
44 p = mp_fork.Process(target=handler, args=(que, func, args), kwargs=kwargs)
46 p.join(timeout=timeout)
52 self.
log.debug(
"terminate function (%s: %s // %s) after timeout is exceeded", func.__name__, args, kwargs)
58 def post_search(self, request:
"SXNG_Request", search:
"SearchWithPlugins") -> EngineResults:
62 if search.search_query.pageno > 1:
65 query = search.search_query.query
71 query = query.replace(
"x",
"*").replace(
":",
"/")
74 word, constants =
"", set()
86 if constants - set(math_constants):
90 ui_locale = babel.Locale.parse(request.preferences.get_value(
"locale"), sep=
"-")
93 def _decimal(match: re.Match) -> str:
94 val = match.string[match.start() : match.end()]
95 val = babel.numbers.parse_decimal(val, ui_locale, numbering_system=
"latn")
98 decimal = ui_locale.number_symbols[
"latn"][
"decimal"]
99 group = ui_locale.number_symbols[
"latn"][
"group"]
100 query = re.sub(f
"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query)
103 query_py_formatted = query.replace(
"^",
"**")
106 res = self.
timeout_func(0.05, _eval_expr, query_py_formatted)
107 if res
is None or res[0] ==
"":
110 res, is_boolean = res
112 res =
"True" if res != 0
else "False"
114 res = babel.numbers.format_decimal(res, locale=ui_locale)
115 results.add(results.types.Answer(answer=f
"{search.search_query.query} = {res}"))
120def _compare(ops: list[ast.cmpop], values: list[int | float]) -> int:
122 2 < 3 becomes ops=[ast.Lt] and values=[2,3]
123 2 < 3 <= 4 becomes ops=[ast.Lt, ast.LtE] and values=[2,3, 4]
125 for op, a, b
in zip(ops, values, values[1:]):
126 if isinstance(op, ast.Eq)
and a == b:
128 if isinstance(op, ast.NotEq)
and a != b:
130 if isinstance(op, ast.Lt)
and a < b:
132 if isinstance(op, ast.LtE)
and a <= b:
134 if isinstance(op, ast.Gt)
and a > b:
136 if isinstance(op, ast.GtE)
and a >= b:
151operators: dict[type, typing.Callable] = {
152 ast.Add: operator.add,
153 ast.Sub: operator.sub,
154 ast.Mult: operator.mul,
155 ast.Div: operator.truediv,
156 ast.Pow: operator.pow,
157 ast.BitXor: operator.xor,
158 ast.BitOr: operator.or_,
159 ast.BitAnd: operator.and_,
160 ast.USub: operator.neg,
161 ast.RShift: operator.rshift,
162 ast.LShift: operator.lshift,
163 ast.Mod: operator.mod,
164 ast.Compare: _compare,
178mp_fork = multiprocessing.get_context(
"fork")
183 Evaluates the given textual expression.
185 Returns a tuple of (numericResult, isBooleanResult).
187 >>> _eval_expr('2^6')
189 >>> _eval_expr('2**6')
191 >>> _eval_expr('1 + 2*3**(4^5) / (6 + -7)')
193 >>> _eval_expr('1 < 3')
195 >>> _eval_expr('5 < 3')
197 >>> _eval_expr('17 == 11+1+5 == 7+5+5')
201 root_expr = ast.parse(expr, mode=
'eval').body
202 return _eval(root_expr), isinstance(root_expr, ast.Compare)
204 except (SyntaxError, TypeError, ZeroDivisionError):
210 if isinstance(node, ast.Constant)
and isinstance(node.value, (int, float)):
213 if isinstance(node, ast.BinOp):
214 return operators[type(node.op)](
_eval(node.left),
_eval(node.right))
216 if isinstance(node, ast.UnaryOp):
217 return operators[type(node.op)](
_eval(node.operand))
219 if isinstance(node, ast.Compare):
222 if isinstance(node, ast.Name)
and node.id
in math_constants:
223 return math_constants[node.id]
225 raise TypeError(node)
228def handler(q: multiprocessing.Queue, func, args, **kwargs):
230 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)