calculator: use subprocess.run
This commit is contained in:
		
							parent
							
								
									1a11282ab4
								
							
						
					
					
						commit
						8da64e1182
					
				
							
								
								
									
										187
									
								
								diff.diff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								diff.diff
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,187 @@ | |||||||
|  | diff --git a/searx/plugins/calculator/__init__.py b/searx/plugins/calculator/__init__.py
 | ||||||
|  | index aeabc9d9f..306a9d554 100644
 | ||||||
|  | --- a/searx/plugins/calculator/__init__.py
 | ||||||
|  | +++ b/searx/plugins/calculator/__init__.py
 | ||||||
|  | @@ -2,6 +2,7 @@
 | ||||||
|  |  """Calculate mathematical expressions using ack#eval | ||||||
|  |  """ | ||||||
|  |   | ||||||
|  | +import decimal
 | ||||||
|  |  import re | ||||||
|  |  import sys | ||||||
|  |  import subprocess | ||||||
|  | @@ -87,9 +88,9 @@ def post_search(_request, search):
 | ||||||
|  |          val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn") | ||||||
|  |          return str(val) | ||||||
|  |   | ||||||
|  | -    decimal = ui_locale.number_symbols["latn"]["decimal"]
 | ||||||
|  | -    group = ui_locale.number_symbols["latn"]["group"]
 | ||||||
|  | -    query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query)
 | ||||||
|  | +    loc_decimal = ui_locale.number_symbols["latn"]["decimal"]
 | ||||||
|  | +    loc_group = ui_locale.number_symbols["latn"]["group"]
 | ||||||
|  | +    query = re.sub(f"[0-9]+[{loc_decimal}|{loc_group}][0-9]+[{loc_decimal}|{loc_group}]?[0-9]?", _decimal, query)
 | ||||||
|  |   | ||||||
|  |      # only numbers and math operators are accepted | ||||||
|  |      if any(str.isalpha(c) for c in query): | ||||||
|  | @@ -102,6 +103,10 @@ def post_search(_request, search):
 | ||||||
|  |      result = call_calculator(query_py_formatted, 0.05) | ||||||
|  |      if result is None or result == "": | ||||||
|  |          return True | ||||||
|  | -    result = babel.numbers.format_decimal(result, locale=ui_locale)
 | ||||||
|  | +    if len(result) < 15:  # arbitrary number, TODO : check the actual limit
 | ||||||
|  | +        try:
 | ||||||
|  | +            result = babel.numbers.format_decimal(result, locale=ui_locale)
 | ||||||
|  | +        except decimal.InvalidOperation:
 | ||||||
|  | +            pass
 | ||||||
|  |      search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"} | ||||||
|  |      return True | ||||||
|  | diff --git a/searx/plugins/calculator/calculator_process.py b/searx/plugins/calculator/calculator_process.py
 | ||||||
|  | index 69d3686c7..2222c12d7 100644
 | ||||||
|  | --- a/searx/plugins/calculator/calculator_process.py
 | ||||||
|  | +++ b/searx/plugins/calculator/calculator_process.py
 | ||||||
|  | @@ -1,44 +1,129 @@
 | ||||||
|  |  # SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | +# pylint: disable=C0301, C0103
 | ||||||
|  |  """Standalone script to actually calculate mathematical expressions using ast | ||||||
|  |   | ||||||
|  |  This is not a module, the SearXNG modules are not available here | ||||||
|  | +
 | ||||||
|  | +Use Decimal instead of float to keep precision
 | ||||||
|  |  """ | ||||||
|  |   | ||||||
|  |  import ast | ||||||
|  |  import sys | ||||||
|  |  import operator | ||||||
|  | +from decimal import Decimal
 | ||||||
|  |  from typing import Callable | ||||||
|  |   | ||||||
|  |   | ||||||
|  | +def _can_be_int(a: Decimal) -> bool:
 | ||||||
|  | +    return -1E10 < a < 1E10
 | ||||||
|  | +
 | ||||||
|  | +
 | ||||||
|  | +def _div(a: int | Decimal, b: int | Decimal) -> int | Decimal:
 | ||||||
|  | +    # If exactly divisible, return int
 | ||||||
|  | +    if isinstance(a, int) and isinstance(b, int) and a % b == 0:
 | ||||||
|  | +        return a // b
 | ||||||
|  | +        
 | ||||||
|  | +    # Otherwise, make sure to use Decimal and divide
 | ||||||
|  | +    result = Decimal(a) / Decimal(b)
 | ||||||
|  | +
 | ||||||
|  | +    # Convert integral Decimal back to int
 | ||||||
|  | +    if _can_be_int(result) and (result % 1) == 0:
 | ||||||
|  | +        return int(result)
 | ||||||
|  | +
 | ||||||
|  | +    #
 | ||||||
|  | +    return result
 | ||||||
|  | +
 | ||||||
|  | +
 | ||||||
|  | +def _compare(ops: list[ast.cmpop], values: list[int | Decimal]) -> int:
 | ||||||
|  | +    """
 | ||||||
|  | +    2 < 3 becomes ops=[ast.Lt] and values=[2,3]
 | ||||||
|  | +    2 < 3 <= 4 becomes ops=[ast.Lt, ast.LtE] and values=[2,3, 4]
 | ||||||
|  | +    """
 | ||||||
|  | +    for op, a, b in zip(ops, values, values[1:]):
 | ||||||
|  | +        if isinstance(op, ast.Eq) and a == b:
 | ||||||
|  | +            continue
 | ||||||
|  | +        if isinstance(op, ast.NotEq) and a != b:
 | ||||||
|  | +            continue
 | ||||||
|  | +        if isinstance(op, ast.Lt) and a < b:
 | ||||||
|  | +            continue
 | ||||||
|  | +        if isinstance(op, ast.LtE) and a <= b:
 | ||||||
|  | +            continue
 | ||||||
|  | +        if isinstance(op, ast.Gt) and a > b:
 | ||||||
|  | +            continue
 | ||||||
|  | +        if isinstance(op, ast.GtE) and a >= b:
 | ||||||
|  | +            continue
 | ||||||
|  | +
 | ||||||
|  | +        # Ignore impossible ops:
 | ||||||
|  | +        # * ast.Is
 | ||||||
|  | +        # * ast.IsNot
 | ||||||
|  | +        # * ast.In
 | ||||||
|  | +        # * ast.NotIn
 | ||||||
|  | +
 | ||||||
|  | +        # the result is False for a and b and operation op
 | ||||||
|  | +        return 0
 | ||||||
|  | +    # the results for all the ops are True
 | ||||||
|  | +    return 1
 | ||||||
|  | +
 | ||||||
|  | +
 | ||||||
|  |  operators: dict[type, Callable] = { | ||||||
|  |      ast.Add: operator.add, | ||||||
|  |      ast.Sub: operator.sub, | ||||||
|  |      ast.Mult: operator.mul, | ||||||
|  | -    ast.Div: operator.truediv,
 | ||||||
|  | +    ast.Div: _div,
 | ||||||
|  | +    ast.FloorDiv: operator.floordiv,
 | ||||||
|  |      ast.Pow: operator.pow, | ||||||
|  |      ast.BitXor: operator.xor, | ||||||
|  | +    ast.BitOr: operator.or_,
 | ||||||
|  | +    ast.BitAnd: operator.and_,
 | ||||||
|  |      ast.USub: operator.neg, | ||||||
|  | +    ast.RShift: operator.rshift,
 | ||||||
|  | +    ast.LShift: operator.lshift,
 | ||||||
|  | +    ast.Mod: operator.mod,
 | ||||||
|  | +    ast.Compare: _compare,
 | ||||||
|  |  } | ||||||
|  |   | ||||||
|  |   | ||||||
|  |  def _eval_expr(expr): | ||||||
|  |      """ | ||||||
|  | -    >>> _eval_expr('2^6')
 | ||||||
|  | +    >>> _eval_expr('2^6')  # impossible since ^ are replaced by **
 | ||||||
|  |      4 | ||||||
|  |      >>> _eval_expr('2**6') | ||||||
|  |      64 | ||||||
|  | -    >>> _eval_expr('1 + 2*3**(4^5) / (6 + -7)')
 | ||||||
|  | -    -5.0
 | ||||||
|  | +    >>> _eval_expr('1 + 2*3**(4 & 5) / (6 + -7)')
 | ||||||
|  | +    -161
 | ||||||
|  | +    >>> _eval_expr('1 + 2*3**(4**5) / 3')
 | ||||||
|  | +    93347962185255010883239938546216647056352444195933501937659232930519760154316312807498422234700994305117191266357868789527181763648040214645337834245702296828547937148565645234701754987989101071392954510261670322199350731379417003085154324599143682904788096680761558781483724014647647071163698385126484050594136951860682645536131764747189062863204448353338035480186155756879682296358215593934265996371329869104231565954993221751726753474814131074299631924687318549069202765175583427589120
 | ||||||
|  | +    >>> _eval_expr('1 + 2*3**(4**5) // 3**3')
 | ||||||
|  | +    5563972126558721714212890776766338768980052721258014556029989298017010697979707289189006223362743515081238226077429580064247951725008500495275129928928750564369436332497933222931728064297980133974132210627893824708423062049115717594930787360617141896771675150439832137911541701236226980159503125257878544962056693545620599123008952423762623242330816289981486527930865034871082442781818842526332497380455128497137520668208195075495645610977061454910017962735135468785119221518968548
 | ||||||
|  | +    >>> _eval_expr('1 + 2*3**(4**5) >> 1620')
 | ||||||
|  | +    16
 | ||||||
|  | +    >>> _eval_expr('2 < 3 < 5')
 | ||||||
|  | +    1
 | ||||||
|  | +    >>> _eval_expr('2 > 3')
 | ||||||
|  | +    0
 | ||||||
|  | +    >>> _eval_expr('5/3 + 6/3 - 5/3')
 | ||||||
|  | +    2
 | ||||||
|  | +    >>> _eval_expr('0.1 + 0.1 + 0.1 - 0.3')
 | ||||||
|  | +    0
 | ||||||
|  |      """ | ||||||
|  |      try: | ||||||
|  | -        return _eval(ast.parse(expr, mode='eval').body)
 | ||||||
|  | +        result = _eval(ast.parse(expr, mode='eval').body)
 | ||||||
|  | +        if isinstance(result, Decimal) and _can_be_int(result) and round(result, 25) == int(result):
 | ||||||
|  | +            # make sure x is x not x.0 (for example 0 instead of 0.0)
 | ||||||
|  | +            result = int(result)
 | ||||||
|  | +        return result
 | ||||||
|  |      except ZeroDivisionError: | ||||||
|  |          # This is undefined | ||||||
|  |          return "" | ||||||
|  | +    except OverflowError:
 | ||||||
|  | +        return ""
 | ||||||
|  |   | ||||||
|  |   | ||||||
|  |  def _eval(node): | ||||||
|  | -    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
 | ||||||
|  | +    if isinstance(node, ast.Constant) and isinstance(node.value, float):
 | ||||||
|  | +        return Decimal(str(node.value))
 | ||||||
|  | +
 | ||||||
|  | +    if isinstance(node, ast.Constant) and isinstance(node.value, int):
 | ||||||
|  |          return node.value | ||||||
|  |   | ||||||
|  |      if isinstance(node, ast.BinOp): | ||||||
|  | @@ -47,6 +132,9 @@ def _eval(node):
 | ||||||
|  |      if isinstance(node, ast.UnaryOp): | ||||||
|  |          return operators[type(node.op)](_eval(node.operand)) | ||||||
|  |   | ||||||
|  | +    if isinstance(node, ast.Compare):
 | ||||||
|  | +        return _compare(node.ops, [_eval(node.left)] + [_eval(c) for c in node.comparators])
 | ||||||
|  | +
 | ||||||
|  |      raise TypeError(node) | ||||||
|  |   | ||||||
|  |   | ||||||
| @ -2,6 +2,7 @@ | |||||||
| """Calculate mathematical expressions using ack#eval | """Calculate mathematical expressions using ack#eval | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
|  | import decimal | ||||||
| import re | import re | ||||||
| import sys | import sys | ||||||
| import subprocess | import subprocess | ||||||
| @ -24,7 +25,6 @@ logger = logger.getChild(plugin_id) | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def call_calculator(query_py_formatted, timeout): | def call_calculator(query_py_formatted, timeout): | ||||||
|     calculator_process_py_path = Path(__file__).parent.absolute() / "calculator_process.py" |  | ||||||
|     # see https://docs.python.org/3/using/cmdline.html |     # see https://docs.python.org/3/using/cmdline.html | ||||||
|     # -S Disable the import of the module site and the site-dependent manipulations |     # -S Disable the import of the module site and the site-dependent manipulations | ||||||
|     #    of sys.path that it entails. Also disable these manipulations if site is |     #    of sys.path that it entails. Also disable these manipulations if site is | ||||||
| @ -33,35 +33,32 @@ def call_calculator(query_py_formatted, timeout): | |||||||
|     # -E Ignore all PYTHON* environment variables, e.g. PYTHONPATH and PYTHONHOME, that might be set. |     # -E Ignore all PYTHON* environment variables, e.g. PYTHONPATH and PYTHONHOME, that might be set. | ||||||
|     # -P Don’t prepend a potentially unsafe path to sys.path |     # -P Don’t prepend a potentially unsafe path to sys.path | ||||||
|     # -s Don’t add the user site-packages directory to sys.path. |     # -s Don’t add the user site-packages directory to sys.path. | ||||||
|     process = subprocess.Popen(  # pylint: disable=R1732 |     calculator_process_py_path = Path(__file__).parent.absolute() / "calculator_process.py" | ||||||
|         [sys.executable, "-S", "-I", calculator_process_py_path, query_py_formatted], |     cmd = [sys.executable, "-S", "-I", str(calculator_process_py_path), query_py_formatted] | ||||||
|         stdout=subprocess.PIPE, | 
 | ||||||
|         stderr=subprocess.PIPE, |     try: | ||||||
|         text=True, |         result = subprocess.run( | ||||||
|  |             cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, check=False | ||||||
|         ) |         ) | ||||||
|     try: | 
 | ||||||
|         stdout, stderr = process.communicate(timeout=timeout) |         if result.returncode == 0 and not result.stderr: | ||||||
|         if process.returncode == 0 and not stderr: |             return result.stdout | ||||||
|             return stdout | 
 | ||||||
|         logger.debug("calculator exited with stderr %s", stderr) |         logger.debug("calculator exited with stderr: %s", result.stderr) | ||||||
|         return None |         return None | ||||||
|     except subprocess.TimeoutExpired: | 
 | ||||||
|         process.terminate() |     except subprocess.TimeoutExpired as e: | ||||||
|         try: |         logger.debug("calculator did not exit in time") | ||||||
|             # Give the process a grace period to terminate |         # subprocess.run automatically attempts to terminate the process on timeout. | ||||||
|             process.communicate(timeout=2) |         # Additional killing is generally not necessary, but we can ensure it if needed. | ||||||
|         except subprocess.TimeoutExpired: | 
 | ||||||
|             # Forcefully kill the process |         # Check if stdout or stderr are captured and log them if available | ||||||
|             process.kill() |         if e.stdout: | ||||||
|             process.communicate() |             logger.debug("Partial stdout before timeout: %s", e.stdout) | ||||||
|         logger.debug("calculator terminated after timeout") |         if e.stderr: | ||||||
|         # Capture any remaining output |             logger.debug("Partial stderr before timeout: %s", e.stderr) | ||||||
|  | 
 | ||||||
|         return None |         return None | ||||||
|     finally: |  | ||||||
|         # Ensure the process is fully cleaned up |  | ||||||
|         if process.poll() is None:  # If still running |  | ||||||
|             process.kill() |  | ||||||
|             process.communicate() |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def post_search(_request, search): | def post_search(_request, search): | ||||||
| @ -87,9 +84,10 @@ def post_search(_request, search): | |||||||
|         val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn") |         val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn") | ||||||
|         return str(val) |         return str(val) | ||||||
| 
 | 
 | ||||||
|     decimal = ui_locale.number_symbols["latn"]["decimal"] |     loc_decimal = ui_locale.number_symbols["latn"]["decimal"] | ||||||
|     group = ui_locale.number_symbols["latn"]["group"] |     loc_group = ui_locale.number_symbols["latn"]["group"] | ||||||
|     query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query) | 
 | ||||||
|  |     query = re.sub(f"[0-9]+[{loc_decimal}|{loc_group}][0-9]+[{loc_decimal}|{loc_group}]?[0-9]?", _decimal, query) | ||||||
| 
 | 
 | ||||||
|     # only numbers and math operators are accepted |     # only numbers and math operators are accepted | ||||||
|     if any(str.isalpha(c) for c in query): |     if any(str.isalpha(c) for c in query): | ||||||
| @ -102,6 +100,10 @@ def post_search(_request, search): | |||||||
|     result = call_calculator(query_py_formatted, 0.05) |     result = call_calculator(query_py_formatted, 0.05) | ||||||
|     if result is None or result == "": |     if result is None or result == "": | ||||||
|         return True |         return True | ||||||
|  |     if len(result) < 15:  # arbitrary number, TODO : check the actual limit | ||||||
|  |         try: | ||||||
|             result = babel.numbers.format_decimal(result, locale=ui_locale) |             result = babel.numbers.format_decimal(result, locale=ui_locale) | ||||||
|  |         except decimal.InvalidOperation: | ||||||
|  |             pass | ||||||
|     search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"} |     search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"} | ||||||
|     return True |     return True | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user