Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-simpleeval for openSUSE:Factory checked in at 2024-01-03 12:23:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-simpleeval (Old) and /work/SRC/openSUSE:Factory/.python-simpleeval.new.28375 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-simpleeval" Wed Jan 3 12:23:57 2024 rev:6 rq:1135607 version:0.9.13 Changes: -------- --- /work/SRC/openSUSE:Factory/python-simpleeval/python-simpleeval.changes 2022-02-23 16:27:09.659510357 +0100 +++ /work/SRC/openSUSE:Factory/.python-simpleeval.new.28375/python-simpleeval.changes 2024-01-03 12:23:57.721228048 +0100 @@ -1,0 +2,12 @@ +Fri Dec 29 09:15:41 UTC 2023 - Dirk Müller <[email protected]> + +- update to 0.9.13: + * Better handling of empty strings passed as input. + * Fix the shift safe number issue from 0.9.12 + * More minor pylint / etc fixes / cleanups (general code + quality) + * separate `.parse` from #115 + * Allow setting up completely empty `{}` operators / functions + * Add extra bit-ops from #87 + +------------------------------------------------------------------- Old: ---- simpleeval-0.9.12.tar.gz New: ---- simpleeval-0.9.13.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-simpleeval.spec ++++++ --- /var/tmp/diff_new_pack.j1dpzp/_old 2024-01-03 12:23:58.365251579 +0100 +++ /var/tmp/diff_new_pack.j1dpzp/_new 2024-01-03 12:23:58.365251579 +0100 @@ -1,7 +1,7 @@ # # spec file # -# Copyright (c) 2022 SUSE LLC +# Copyright (c) 2023 SUSE LLC # Copyright (c) 2015 Dr. Axel Braun # # All modifications and additions to the file contributed by third parties @@ -20,7 +20,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define modname simpleeval Name: python-%{modname} -Version: 0.9.12 +Version: 0.9.13 Release: 0 Summary: A simple, safe single expression evaluator library License: MIT ++++++ simpleeval-0.9.12.tar.gz -> simpleeval-0.9.13.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/Makefile new/simpleeval-0.9.13/Makefile --- old/simpleeval-0.9.12/Makefile 2022-01-15 17:00:46.000000000 +0100 +++ new/simpleeval-0.9.13/Makefile 2023-02-17 09:21:11.000000000 +0100 @@ -24,6 +24,8 @@ lint: black --check --diff simpleeval.py test_simpleeval.py isort --check-only --diff simpleeval.py test_simpleeval.py + pylint simpleeval.py test_simpleeval.py + mypy simpleeval.py test_simpleeval.py format: black simpleeval.py test_simpleeval.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/PKG-INFO new/simpleeval-0.9.13/PKG-INFO --- old/simpleeval-0.9.12/PKG-INFO 2022-01-15 18:31:14.599136400 +0100 +++ new/simpleeval-0.9.13/PKG-INFO 2023-02-17 09:24:18.891371300 +0100 @@ -1,13 +1,11 @@ Metadata-Version: 2.1 Name: simpleeval -Version: 0.9.12 +Version: 0.9.13 Summary: A simple, safe single expression evaluator library. Home-page: https://github.com/danthedeckie/simpleeval Author: Daniel Fairhead Author-email: [email protected] -License: UNKNOWN Keywords: eval,simple,expression,parse,ast -Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License @@ -31,7 +29,14 @@ :target: https://badge.fury.io/py/simpleeval :alt: PyPI Version -A quick single file library for easily adding evaluatable expressions into +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + +.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen + :target: https://github.com/PyCQA/pylint + + +A single file library for easily adding evaluatable expressions into python projects. Say you want to allow a user to set an alarm volume, which could depend on the time of day, alarm level, how many previous alarms had gone off, and if there is music playing at the time. @@ -39,8 +44,9 @@ Or if you want to allow simple formulae in a web application, but don't want to give full eval() access, or don't want to run in javascript on the client side. -It's deliberately very simple, pull it in from PyPI (pip or easy_install), or -even just a single file you can dump into a project. +It's deliberately trying to stay simple to use and not have millions of features, +pull it in from PyPI (pip or easy_install), or even just a single file you can dump +into a project. Internally, it's using the amazing python ``ast`` module to parse the expression, which allows very fine control of what is and isn't allowed. It @@ -55,7 +61,7 @@ You should be aware of this when deploying in a public setting. -The defaults are pretty locked down and basic, and it's very easy to add +The defaults are pretty locked down and basic, and it's easy to add whatever extra specific functionality you need (your own functions, variable/name lookup, etc). @@ -149,23 +155,36 @@ | | ``"spam" in "my breakfast"`` | | | -> ``False`` | +--------+------------------------------------+ +| ``^`` | "bitwise exclusive OR" (xor) | +| | ``62 ^ 20`` -> ``42`` | ++--------+------------------------------------+ +| ``|`` | "bitwise OR" | +| | ``8 | 34`` -> ``42`` | ++--------+------------------------------------+ +| ``&`` | "bitwise AND" | +| | ``100 & 63`` -> ``36`` | ++--------+------------------------------------+ +| ``~`` | "bitwise invert" | +| | ``~ -43`` -> ``42`` | ++--------+------------------------------------+ -The ``^`` operator is notably missing - not because it's hard, but because it -is often mistaken for a exponent operator, not the bitwise operation that it is -in python. It's trivial to add back in again if you wish (using the class -based evaluator explained below): +The ``^`` operator is often mistaken for a exponent operator, not the bitwise +operation that it is in python, so if you want ``3 ^ 2`` to equal ``9``, you can +replace the operator like this: .. code-block:: python >>> import ast - >>> import operator + >>> from simpleeval import safe_power >>> s = SimpleEval() - >>> s.operators[ast.BitXor] = operator.xor + >>> s.operators[ast.BitXor] = safe_power + + >>> s.eval("3 ^ 2") + 9 - >>> s.eval("2 ^ 10") - 8 +for example. Limited Power ~~~~~~~~~~~~~ @@ -305,6 +324,22 @@ # and so on... +One useful feature of using the ``SimpleEval`` object is that you can parse an expression +once, and then evaluate it mulitple times using different ``names``: + +.. code-block:: python + + # Set up & Cache the parse tree: + expression = "foo + bar" + parsed = s.parse(expression) + + # evaluate the expression multiple times: + for names in [{"foo": 1, "bar": 10}, {"foo": 100, "bar": 42}]: + s.names = names + print(s.eval(expression, previously_parsed=parsed)) + +for instance. This may help with performance. + You can assign / edit the various options of the ``SimpleEval`` object if you want to. Either assign them during creation (like the ``simple_eval`` function) @@ -418,11 +453,18 @@ (requires ``entr``) +I'm trying to keep the codebase relatively clean with Black, isort, pylint & mypy. +See:: + + $ make format + +and:: + + $ make lint + BEWARE ------ -I've done the best I can with this library - but there's no warrenty, no guarentee, nada. A lot of +I've done the best I can with this library - but there's no warranty, no guarantee, nada. A lot of very clever people think the whole idea of trying to sandbox CPython is impossible. Read the code yourself, and use it at your own risk. - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/README.rst new/simpleeval-0.9.13/README.rst --- old/simpleeval-0.9.12/README.rst 2022-01-15 16:40:37.000000000 +0100 +++ new/simpleeval-0.9.13/README.rst 2023-02-17 09:23:37.000000000 +0100 @@ -13,7 +13,14 @@ :target: https://badge.fury.io/py/simpleeval :alt: PyPI Version -A quick single file library for easily adding evaluatable expressions into +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + +.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen + :target: https://github.com/PyCQA/pylint + + +A single file library for easily adding evaluatable expressions into python projects. Say you want to allow a user to set an alarm volume, which could depend on the time of day, alarm level, how many previous alarms had gone off, and if there is music playing at the time. @@ -21,8 +28,9 @@ Or if you want to allow simple formulae in a web application, but don't want to give full eval() access, or don't want to run in javascript on the client side. -It's deliberately very simple, pull it in from PyPI (pip or easy_install), or -even just a single file you can dump into a project. +It's deliberately trying to stay simple to use and not have millions of features, +pull it in from PyPI (pip or easy_install), or even just a single file you can dump +into a project. Internally, it's using the amazing python ``ast`` module to parse the expression, which allows very fine control of what is and isn't allowed. It @@ -37,7 +45,7 @@ You should be aware of this when deploying in a public setting. -The defaults are pretty locked down and basic, and it's very easy to add +The defaults are pretty locked down and basic, and it's easy to add whatever extra specific functionality you need (your own functions, variable/name lookup, etc). @@ -131,23 +139,36 @@ | | ``"spam" in "my breakfast"`` | | | -> ``False`` | +--------+------------------------------------+ +| ``^`` | "bitwise exclusive OR" (xor) | +| | ``62 ^ 20`` -> ``42`` | ++--------+------------------------------------+ +| ``|`` | "bitwise OR" | +| | ``8 | 34`` -> ``42`` | ++--------+------------------------------------+ +| ``&`` | "bitwise AND" | +| | ``100 & 63`` -> ``36`` | ++--------+------------------------------------+ +| ``~`` | "bitwise invert" | +| | ``~ -43`` -> ``42`` | ++--------+------------------------------------+ -The ``^`` operator is notably missing - not because it's hard, but because it -is often mistaken for a exponent operator, not the bitwise operation that it is -in python. It's trivial to add back in again if you wish (using the class -based evaluator explained below): +The ``^`` operator is often mistaken for a exponent operator, not the bitwise +operation that it is in python, so if you want ``3 ^ 2`` to equal ``9``, you can +replace the operator like this: .. code-block:: python >>> import ast - >>> import operator + >>> from simpleeval import safe_power >>> s = SimpleEval() - >>> s.operators[ast.BitXor] = operator.xor + >>> s.operators[ast.BitXor] = safe_power - >>> s.eval("2 ^ 10") - 8 + >>> s.eval("3 ^ 2") + 9 + +for example. Limited Power ~~~~~~~~~~~~~ @@ -287,6 +308,22 @@ # and so on... +One useful feature of using the ``SimpleEval`` object is that you can parse an expression +once, and then evaluate it mulitple times using different ``names``: + +.. code-block:: python + + # Set up & Cache the parse tree: + expression = "foo + bar" + parsed = s.parse(expression) + + # evaluate the expression multiple times: + for names in [{"foo": 1, "bar": 10}, {"foo": 100, "bar": 42}]: + s.names = names + print(s.eval(expression, previously_parsed=parsed)) + +for instance. This may help with performance. + You can assign / edit the various options of the ``SimpleEval`` object if you want to. Either assign them during creation (like the ``simple_eval`` function) @@ -400,9 +437,18 @@ (requires ``entr``) +I'm trying to keep the codebase relatively clean with Black, isort, pylint & mypy. +See:: + + $ make format + +and:: + + $ make lint + BEWARE ------ -I've done the best I can with this library - but there's no warrenty, no guarentee, nada. A lot of +I've done the best I can with this library - but there's no warranty, no guarantee, nada. A lot of very clever people think the whole idea of trying to sandbox CPython is impossible. Read the code yourself, and use it at your own risk. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/pyproject.toml new/simpleeval-0.9.13/pyproject.toml --- old/simpleeval-0.9.12/pyproject.toml 2022-01-15 17:27:18.000000000 +0100 +++ new/simpleeval-0.9.13/pyproject.toml 2023-02-17 09:21:11.000000000 +0100 @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=30.3.0", "wheel", "build"] +requires = ["setuptools>=30.3.0", "wheel"] build-backend = "setuptools.build_meta" [tool.black] @@ -16,7 +16,9 @@ [tool.pylint.messages_control] disable = [ + "fixme", "consider-using-f-string", + "raise-missing-from", "invalid-name", "too-few-public-methods", "too-many-public-methods", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/setup.cfg new/simpleeval-0.9.13/setup.cfg --- old/simpleeval-0.9.12/setup.cfg 2022-01-15 18:31:14.599745300 +0100 +++ new/simpleeval-0.9.13/setup.cfg 2023-02-17 09:24:18.892493500 +0100 @@ -1,6 +1,6 @@ [metadata] name = simpleeval -version = 0.9.12 +version = 0.9.13 author = Daniel Fairhead author_email = [email protected] description = A simple, safe single expression evaluator library. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/simpleeval.egg-info/PKG-INFO new/simpleeval-0.9.13/simpleeval.egg-info/PKG-INFO --- old/simpleeval-0.9.12/simpleeval.egg-info/PKG-INFO 2022-01-15 18:31:14.000000000 +0100 +++ new/simpleeval-0.9.13/simpleeval.egg-info/PKG-INFO 2023-02-17 09:24:18.000000000 +0100 @@ -1,13 +1,11 @@ Metadata-Version: 2.1 Name: simpleeval -Version: 0.9.12 +Version: 0.9.13 Summary: A simple, safe single expression evaluator library. Home-page: https://github.com/danthedeckie/simpleeval Author: Daniel Fairhead Author-email: [email protected] -License: UNKNOWN Keywords: eval,simple,expression,parse,ast -Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License @@ -31,7 +29,14 @@ :target: https://badge.fury.io/py/simpleeval :alt: PyPI Version -A quick single file library for easily adding evaluatable expressions into +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + +.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen + :target: https://github.com/PyCQA/pylint + + +A single file library for easily adding evaluatable expressions into python projects. Say you want to allow a user to set an alarm volume, which could depend on the time of day, alarm level, how many previous alarms had gone off, and if there is music playing at the time. @@ -39,8 +44,9 @@ Or if you want to allow simple formulae in a web application, but don't want to give full eval() access, or don't want to run in javascript on the client side. -It's deliberately very simple, pull it in from PyPI (pip or easy_install), or -even just a single file you can dump into a project. +It's deliberately trying to stay simple to use and not have millions of features, +pull it in from PyPI (pip or easy_install), or even just a single file you can dump +into a project. Internally, it's using the amazing python ``ast`` module to parse the expression, which allows very fine control of what is and isn't allowed. It @@ -55,7 +61,7 @@ You should be aware of this when deploying in a public setting. -The defaults are pretty locked down and basic, and it's very easy to add +The defaults are pretty locked down and basic, and it's easy to add whatever extra specific functionality you need (your own functions, variable/name lookup, etc). @@ -149,23 +155,36 @@ | | ``"spam" in "my breakfast"`` | | | -> ``False`` | +--------+------------------------------------+ +| ``^`` | "bitwise exclusive OR" (xor) | +| | ``62 ^ 20`` -> ``42`` | ++--------+------------------------------------+ +| ``|`` | "bitwise OR" | +| | ``8 | 34`` -> ``42`` | ++--------+------------------------------------+ +| ``&`` | "bitwise AND" | +| | ``100 & 63`` -> ``36`` | ++--------+------------------------------------+ +| ``~`` | "bitwise invert" | +| | ``~ -43`` -> ``42`` | ++--------+------------------------------------+ -The ``^`` operator is notably missing - not because it's hard, but because it -is often mistaken for a exponent operator, not the bitwise operation that it is -in python. It's trivial to add back in again if you wish (using the class -based evaluator explained below): +The ``^`` operator is often mistaken for a exponent operator, not the bitwise +operation that it is in python, so if you want ``3 ^ 2`` to equal ``9``, you can +replace the operator like this: .. code-block:: python >>> import ast - >>> import operator + >>> from simpleeval import safe_power >>> s = SimpleEval() - >>> s.operators[ast.BitXor] = operator.xor + >>> s.operators[ast.BitXor] = safe_power + + >>> s.eval("3 ^ 2") + 9 - >>> s.eval("2 ^ 10") - 8 +for example. Limited Power ~~~~~~~~~~~~~ @@ -305,6 +324,22 @@ # and so on... +One useful feature of using the ``SimpleEval`` object is that you can parse an expression +once, and then evaluate it mulitple times using different ``names``: + +.. code-block:: python + + # Set up & Cache the parse tree: + expression = "foo + bar" + parsed = s.parse(expression) + + # evaluate the expression multiple times: + for names in [{"foo": 1, "bar": 10}, {"foo": 100, "bar": 42}]: + s.names = names + print(s.eval(expression, previously_parsed=parsed)) + +for instance. This may help with performance. + You can assign / edit the various options of the ``SimpleEval`` object if you want to. Either assign them during creation (like the ``simple_eval`` function) @@ -418,11 +453,18 @@ (requires ``entr``) +I'm trying to keep the codebase relatively clean with Black, isort, pylint & mypy. +See:: + + $ make format + +and:: + + $ make lint + BEWARE ------ -I've done the best I can with this library - but there's no warrenty, no guarentee, nada. A lot of +I've done the best I can with this library - but there's no warranty, no guarantee, nada. A lot of very clever people think the whole idea of trying to sandbox CPython is impossible. Read the code yourself, and use it at your own risk. - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/simpleeval.py new/simpleeval-0.9.13/simpleeval.py --- old/simpleeval-0.9.12/simpleeval.py 2022-01-15 18:18:56.000000000 +0100 +++ new/simpleeval-0.9.13/simpleeval.py 2023-02-17 09:21:11.000000000 +0100 @@ -1,5 +1,5 @@ """ -SimpleEval - (C) 2013-2022 Daniel Fairhead +SimpleEval - (C) 2013-2023 Daniel Fairhead ------------------------------------- An short, easy to use, safe and reasonably extensible expression evaluator. @@ -49,12 +49,14 @@ - mommothazaz123 (Andrew Zhu) f"string" support, Python 3.8 support - lubieowoce (Uryga) various potential vulnerabilities - JCavallo (Jean Cavallo) names dict shouldn't be modified -- Birne94 (Daniel Birnstiel) for fixing leaking generators. +- Birne94 (Daniel Birnstiel) for fixing leaking generators, star expressions - patricksurry (Patrick Surry) or should return last value, even if falsy. - shughes-uk (Samantha Hughes) python w/o 'site' should not fail to import. -- KOLANICH packaging / deployment / setup help & << + >> ops +- KOLANICH packaging / deployment / setup help & << + >> & other bit ops - graingert (Thomas Grainger) packaging / deployment / setup help - bozokopic (Bozo Kopic) Memory leak fix +- daxamin (Dax Amin) Better error for attempting to eval empty string +- smurfix (Matthias Urlichs) Allow clearing functions / operators / etc completely ------------------------------------- Basic Usage: @@ -101,6 +103,7 @@ from random import random PYTHON3 = sys.version_info[0] == 3 +PYTHON35 = PYTHON3 and sys.version_info > (3, 5) ######################################## # Module wide 'globals' @@ -109,6 +112,7 @@ MAX_COMPREHENSION_LENGTH = 10000 MAX_POWER = 4000000 # highest exponent MAX_SHIFT = 10000 # highest << or >> (lshift / rshift) +MAX_SHIFT_BASE = int(sys.float_info.max) # highest on left side of << or >> DISALLOW_PREFIXES = ["_", "func_"] DISALLOW_METHODS = ["format", "format_map", "mro"] @@ -121,7 +125,7 @@ # builtins is a dict in python >3.6 but a module before DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open} if hasattr(__builtins__, "help") or ( - hasattr(__builtins__, "__contains__") and "help" in __builtins__ + hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore ): # PyInstaller environment doesn't include this module. DISALLOW_FUNCTIONS.add(help) @@ -179,6 +183,17 @@ super(InvalidExpression, self).__init__(self.message) +class OperatorNotDefined(InvalidExpression): + """operator does not exist""" + + def __init__(self, attr, expression): + self.message = "Operator '{0}' does not exist in expression '{1}'".format(attr, expression) + self.attr = attr + self.expression = expression + + super(InvalidExpression, self).__init__(self.message) + + class FeatureNotAvailable(InvalidExpression): """What you're trying to do is not allowed.""" @@ -204,6 +219,12 @@ pass +class MultipleExpressions(UserWarning): + """Only the first expression parsed will be used""" + + pass + + ######################################## # Default simple functions to include: @@ -219,7 +240,7 @@ if abs(a) > MAX_POWER or abs(b) > MAX_POWER: raise NumberTooHigh("Sorry! I don't want to evaluate {0} ** {1}".format(a, b)) - return a ** b + return a**b def safe_mult(a, b): # pylint: disable=invalid-name @@ -245,15 +266,15 @@ def safe_rshift(a, b): # pylint: disable=invalid-name - """rshift, but with the maximum""" - if abs(b) > MAX_SHIFT: + """rshift, but with input limits""" + if abs(b) > MAX_SHIFT or abs(a) > MAX_SHIFT_BASE: raise NumberTooHigh("Sorry! I don't want to evaluate {0} >> {1}".format(a, b)) return a >> b def safe_lshift(a, b): # pylint: disable=invalid-name - """lshift, but with the maximum""" - if abs(b) > MAX_SHIFT: + """lshift, but with input limits""" + if abs(b) > MAX_SHIFT or abs(a) > MAX_SHIFT_BASE: raise NumberTooHigh("Sorry! I don't want to evaluate {0} << {1}".format(a, b)) return a << b @@ -280,6 +301,10 @@ ast.Not: op.not_, ast.USub: op.neg, ast.UAdd: op.pos, + ast.BitXor: op.xor, + ast.BitOr: op.or_, + ast.BitAnd: op.and_, + ast.Invert: op.invert, ast.In: lambda x, y: op.contains(y, x), ast.NotIn: lambda x, y: not op.contains(y, x), ast.Is: lambda x, y: x is y, @@ -291,7 +316,8 @@ "randint": random_int, "int": int, "float": float, - "str": str if PYTHON3 else unicode, + # pylint: disable=undefined-variable + "str": str if PYTHON3 else unicode, # type: ignore } DEFAULT_NAMES = {"True": True, "False": False, "None": None} @@ -317,11 +343,11 @@ Create the evaluator instance. Set up valid operators (+,-, etc) functions (add, random, get_val, whatever) and names.""" - if not operators: + if operators is None: operators = DEFAULT_OPERATORS.copy() - if not functions: + if functions is None: functions = DEFAULT_FUNCTIONS.copy() - if not names: + if names is None: names = DEFAULT_NAMES.copy() self.operators = operators @@ -377,16 +403,29 @@ def __del__(self): self.nodes = None - def eval(self, expr): + @staticmethod + def parse(expr): + """parse an expression into a node tree""" + + parsed = ast.parse(expr.strip()) + + if not parsed.body: + raise InvalidExpression("Sorry, cannot evaluate empty string") + if len(parsed.body) > 1: + warnings.warn( + "'{}' contains multiple expressions. Only the first will be used.".format(expr), + MultipleExpressions, + ) + return parsed.body[0] + + def eval(self, expr, previously_parsed=None): """evaluate an expresssion, using the operators, functions and names previously set up.""" # set a copy of the expression aside, so we can give nice errors... - self.expr = expr - # and evaluate: - return self._eval(ast.parse(expr.strip()).body[0]) + return self._eval(previously_parsed or self.parse(expr)) def _eval(self, node): """The internal evaluator used on each node in the parsed tree.""" @@ -415,7 +454,8 @@ ) return self._eval(node.value) - def _eval_import(self, node): + @staticmethod + def _eval_import(node): raise FeatureNotAvailable("Sorry, 'import' is not allowed.") @staticmethod @@ -441,25 +481,32 @@ return node.value def _eval_unaryop(self, node): - return self.operators[type(node.op)](self._eval(node.operand)) + try: + operator = self.operators[type(node.op)] + except KeyError: + raise OperatorNotDefined(node.op, self.expr) + return operator(self._eval(node.operand)) def _eval_binop(self, node): - return self.operators[type(node.op)](self._eval(node.left), self._eval(node.right)) + try: + operator = self.operators[type(node.op)] + except KeyError: + raise OperatorNotDefined(node.op, self.expr) + return operator(self._eval(node.left), self._eval(node.right)) def _eval_boolop(self, node): + to_return = False if isinstance(node.op, ast.And): - vout = False for value in node.values: - vout = self._eval(value) - if not vout: - return vout - return vout + to_return = self._eval(value) + if not to_return: + break elif isinstance(node.op, ast.Or): for value in node.values: - vout = self._eval(value) - if vout: - return vout - return vout + to_return = self._eval(value) + if to_return: + break + return to_return def _eval_compare(self, node): right = self._eval(node.left) @@ -483,7 +530,7 @@ func = self.functions[node.func.id] except KeyError: raise FunctionNotDefined(node.func.id, self.expr) - except AttributeError as e: + except AttributeError: raise FeatureNotAvailable("Lambda Functions not implemented") if func in DISALLOW_FUNCTIONS: @@ -505,14 +552,13 @@ # pass that to ast.parse) if hasattr(self.names, "__getitem__"): return self.names[node.id] - elif callable(self.names): + if callable(self.names): return self.names(node) - else: - raise InvalidExpression( - 'Trying to use name (variable) "{0}"' - ' when no "names" defined for' - " evaluator".format(node.id) - ) + raise InvalidExpression( + 'Trying to use name (variable) "{0}"' + ' when no "names" defined for' + " evaluator".format(node.id) + ) except KeyError: if node.id in self.functions: @@ -523,10 +569,9 @@ def _eval_subscript(self, node): container = self._eval(node.value) key = self._eval(node.slice) - try: - return container[key] - except KeyError: - raise + # Currently if there's a KeyError, that gets raised straight up. + # TODO: Should that be wrapped in an InvalidExpression? + return container[key] def _eval_attribute(self, node): for prefix in DISALLOW_PREFIXES: @@ -595,6 +640,8 @@ function editions. (list, tuple, dict, set). """ + _max_count = 0 + def __init__(self, operators=None, functions=None, names=None): super(EvalWithCompoundTypes, self).__init__(operators, functions, names) @@ -611,18 +658,36 @@ } ) - def eval(self, expr): + def eval(self, expr, previously_parsed=None): + # reset _max_count for each eval run self._max_count = 0 - return super(EvalWithCompoundTypes, self).eval(expr) + return super(EvalWithCompoundTypes, self).eval(expr, previously_parsed) def _eval_dict(self, node): - return {self._eval(k): self._eval(v) for (k, v) in zip(node.keys, node.values)} + result = {} - def _eval_tuple(self, node): - return tuple(self._eval(x) for x in node.elts) + for key, value in zip(node.keys, node.values): + if PYTHON35 and key is None: + # "{**x}" gets parsed as a key-value pair of (None, Name(x)) + result.update(self._eval(value)) + else: + result[self._eval(key)] = self._eval(value) + + return result def _eval_list(self, node): - return list(self._eval(x) for x in node.elts) + result = [] + + for item in node.elts: + if PYTHON3 and isinstance(item, ast.Starred): + result.extend(self._eval(item.value)) + else: + result.append(self._eval(item)) + + return result + + def _eval_tuple(self, node): + return tuple(self._eval(x) for x in node.elts) def _eval_set(self, node): return set(self._eval(x) for x in node.elts) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/simpleeval-0.9.12/test_simpleeval.py new/simpleeval-0.9.13/test_simpleeval.py --- old/simpleeval-0.9.12/test_simpleeval.py 2022-01-15 18:22:19.000000000 +0100 +++ new/simpleeval-0.9.13/test_simpleeval.py 2023-02-17 09:21:11.000000000 +0100 @@ -1,4 +1,4 @@ -# pylint: disable=too-many-public-methods, missing-docstring, too-many-lines, use-of-eval, disallowed-variable, no-self-use +# pylint: disable=too-many-public-methods, missing-docstring, eval-used, too-many-lines, no-self-use, disallowed-name, unspecified-encoding """ Unit tests for simpleeval. @@ -24,6 +24,7 @@ FunctionNotDefined, InvalidExpression, NameNotDefined, + OperatorNotDefined, SimpleEval, simple_eval, ) @@ -82,6 +83,13 @@ -10, ) + def test_bit_ops(self): + self.t("62 ^ 20", 42) + self.t("62 ^ 100", 90) + self.t("8 | 34", 42) + self.t("100 & 63", 36) + self.t("~ -43", 42) + def test_not(self): self.t("not False", True) self.t("not True", False) @@ -109,6 +117,7 @@ self.t("1 < 2 < 3 < 4", 1 < 2 < 3 < 4) self.t("1 < 2 > 3 < 4", 1 < 2 > 3 < 4) + # pylint: disable=comparison-with-itself self.t("1<2<1+1", 1 < 2 < 1 + 1) self.t("1 == 1 == 2", 1 == 1 == 2) self.t("1 == 1 < 2", 1 == 1 < 2) @@ -189,6 +198,49 @@ with self.assertRaises(FeatureNotAvailable): self.t("{22}", False) + def test_empty_string_not_allowed(self): + with self.assertRaises(InvalidExpression): + self.t("", False) + + +class TestEvaluator(DRYTest): + """Tests for how the SimpleEval class does things""" + + def test_only_evalutate_first_statement(self): + # it only evaluates the first statement: + with warnings.catch_warnings(record=True) as ws: + self.t("11; x = 21; x + x", 11) + self.assertIsInstance(ws[0].message, simpleeval.MultipleExpressions) + + def test_parse_and_use_previously_parsed(self): + expr = "x + x" + nodes = self.s.parse(expr) + self.s.names = {"x": 21} + self.assertEqual(self.s.eval(expr, nodes), 42) + + # This can all be done with unittest.mock.patch in python3.3+ - when we drop + # python2 - we can drop this nonsense. + class MockedCalled(Exception): + pass + + def go_boom(*args, **kwargs): + raise MockedCalled("you should never see this.") + + self.s.parse = go_boom + + # Prove the mock is installed in self.s + with self.assertRaises(MockedCalled): + self.s.eval("10 + 10") + + # Prove it's not installed in the actual SimpleEval + SimpleEval().eval("10 + 10") + + # Now running .eval with a previously parsed + self.assertEqual(self.s.eval(expr, previously_parsed=nodes), 42) + + self.s.names = {"x": 100} + self.assertEqual(self.s.eval(expr, nodes), 200) + class TestFunctions(DRYTest): """Functions for expressions to play with""" @@ -258,29 +310,29 @@ self.t("foo()", 42) def test_function_args_required(self): - def foo(toret): - return toret + def foo(to_return): + return to_return self.s.functions["foo"] = foo with self.assertRaises(TypeError): self.t("foo()", 42) self.t("foo(12)", 12) - self.t("foo(toret=100)", 100) + self.t("foo(to_return=100)", 100) def test_function_args_defaults(self): - def foo(toret=9999): - return toret + def foo(to_return=9999): + return to_return self.s.functions["foo"] = foo self.t("foo()", 9999) self.t("foo(12)", 12) - self.t("foo(toret=100)", 100) + self.t("foo(to_return=100)", 100) def test_function_args_bothtypes(self): - def foo(mult, toret=100): - return toret * mult + def foo(mult, to_return=100): + return to_return * mult self.s.functions["foo"] = foo with self.assertRaises(TypeError): @@ -289,10 +341,10 @@ self.t("foo(2)", 200) with self.assertRaises(TypeError): - self.t("foo(toret=100)", 100) + self.t("foo(to_return=100)", 100) - self.t("foo(4, toret=4)", 16) - self.t("foo(mult=2, toret=4)", 8) + self.t("foo(4, to_return=4)", 16) + self.t("foo(mult=2, to_return=4)", 8) self.t("foo(2, 10)", 20) @@ -328,7 +380,7 @@ """exponent operations can take a long time.""" old_max = simpleeval.MAX_POWER - self.t("9**9**5", 9 ** 9 ** 5) + self.t("9**9**5", 9**9**5) with self.assertRaises(simpleeval.NumberTooHigh): self.t("9**9**8", 0) @@ -350,8 +402,14 @@ self.t("1<<25000", 0) with self.assertRaises(simpleeval.NumberTooHigh): + self.t("%s<<25" % (simpleeval.MAX_SHIFT_BASE + 1), 0) + + with self.assertRaises(simpleeval.NumberTooHigh): self.t("1>>25000", 0) + with self.assertRaises(simpleeval.NumberTooHigh): + self.t("%s>>25" % (simpleeval.MAX_SHIFT_BASE + 1), 0) + # and test we can change it: old_max = simpleeval.MAX_SHIFT @@ -420,11 +478,6 @@ with self.assertRaises(simpleeval.IterableTooLong): self.t("('spam spam spam' * 5000).split() * 5000", None) - def test_python_stuff(self): - """other various pythony things.""" - # it only evaluates the first statement: - self.t("11; x = 21; x + x", 11) - def test_function_globals_breakout(self): """by accessing function.__globals__ or func_...""" # thanks perkinslr. @@ -559,6 +612,24 @@ self.t('{"a": 24}.get("b", 11)', 11) self.t('"a" in {"a": 24}', True) + @unittest.skipIf(not simpleeval.PYTHON35, "feature not supported") + def test_dict_star_expression(self): + self.s.names["x"] = {"a": 1, "b": 2} + self.t('{"a": 0, **x, "c": 3}', {"a": 1, "b": 2, "c": 3}) + + # and multiple star expressions should be fine too... + self.s.names["y"] = {"x": 1, "y": 2} + self.t('{"a": 0, **x, **y, "c": 3}', {"a": 1, "b": 2, "c": 3, "x": 1, "y": 2}) + + @unittest.skipIf(not simpleeval.PYTHON35, "feature not supported") + def test_dict_invalid_star_expression(self): + self.s.names["x"] = {"a": 1, "b": 2} + self.s.names["y"] = {"x": 1, "y": 2} + self.s.names["z"] = 42 + + with self.assertRaises(TypeError): + self.t('{"a": 0, **x, **y, **z, "c": 3}', {"a": 1, "b": 2, "c": 3}) + def test_tuple(self): self.t("()", ()) self.t("(1,)", (1,)) @@ -589,6 +660,19 @@ self.t('"b" in ["a","b"]', True) + @unittest.skipIf(not simpleeval.PYTHON3, "feature not supported") + def test_list_star_expression(self): + self.s.names["x"] = [1, 2, 3] + self.t('["a", *x, "b"]', ["a", 1, 2, 3, "b"]) + + @unittest.skipIf(not simpleeval.PYTHON3, "feature not supported") + def test_list_invalid_star_expression(self): + self.s.names["x"] = [1, 2, 3] + self.s.names["y"] = 42 + + with self.assertRaises(TypeError): + self.t('["a", *x, *y, "b"]', ["a", 1, 2, 3, "b"]) + def test_set(self): self.t("{1}", {1}) self.t("{1, 2, 1, 2, 1, 2, 1}", {1, 2}) @@ -708,9 +792,11 @@ self.s.names["s"] = 21 + # or if you attempt to assign an unknown name to another with self.assertRaises(NameNotDefined): with warnings.catch_warnings(record=True) as ws: self.t("s += a", 21) + self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) self.s.names = None @@ -735,6 +821,7 @@ # however, you can't assign to those names: with warnings.catch_warnings(record=True) as ws: self.t("a = 200", 200) + self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) self.assertEqual(self.s.names["a"], 42) @@ -744,6 +831,7 @@ with warnings.catch_warnings(record=True) as ws: self.t("b[0] = 11", 11) + self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) self.assertEqual(self.s.names["b"], [0]) @@ -766,6 +854,7 @@ with warnings.catch_warnings(record=True) as ws: self.t("c['b'] = 99", 99) + self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) self.assertFalse("b" in self.s.names["c"]) @@ -775,6 +864,7 @@ with warnings.catch_warnings(record=True) as ws: self.t("c['c']['c'] = 21", 21) + self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) self.assertEqual(self.s.names["c"]["c"]["c"], 11) @@ -789,6 +879,7 @@ with warnings.catch_warnings(record=True) as ws: self.t("a.b.c = 11", 11) + self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) self.assertEqual(self.s.names["a"]["b"]["c"], 42) @@ -816,6 +907,7 @@ def test_object(self): """using an object for name lookup""" + # pylint: disable=attribute-defined-outside-init class TestObject(object): @staticmethod @@ -1032,10 +1124,12 @@ return BinaryExpression("LT") b = Blah() - self.s.names = {"b": b} - # This should not crash: - e = eval("b > 2", self.s.names) + # These should not crash: + self.assertEqual(b > 2, BinaryExpression("GT")) + self.assertEqual(b < 2, BinaryExpression("LT")) + # And should also work in simpleeval + self.s.names = {"b": b} self.t("b > 2", BinaryExpression("GT")) self.t("1 < 5 > b", BinaryExpression("LT")) @@ -1056,7 +1150,7 @@ self.assertEqual(m.anything, 42) with self.assertRaises(NotImplementedError): - m["nothing"] + m["nothing"] # pylint: disable=pointless-statement self.s.names = {"m": m} self.t("m.anything", 42) @@ -1108,6 +1202,7 @@ def test_functions_are_disallowed_at_init(self): DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open] if simpleeval.PYTHON3: + # pylint: disable=exec-used exec("DISALLOWED.append(exec)") # exec is not a function in Python2... for f in simpleeval.DISALLOW_FUNCTIONS: @@ -1115,12 +1210,13 @@ for x in DISALLOWED: with self.assertRaises(FeatureNotAvailable): - s = SimpleEval(functions={"foo": x}) + SimpleEval(functions={"foo": x}) def test_functions_are_disallowed_in_expressions(self): DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open] if simpleeval.PYTHON3: + # pylint: disable=exec-used exec("DISALLOWED.append(exec)") # exec is not a function in Python2... for f in simpleeval.DISALLOW_FUNCTIONS: @@ -1138,11 +1234,13 @@ simpleeval.DEFAULT_FUNCTIONS = DF.copy() [email protected](simpleeval.PYTHON3 != True, "Python2 fails - but it's not supported anyway.") [email protected](simpleeval.PYTHON3 is not True, "Python2 fails - but it's not supported anyway.") @unittest.skipIf(platform.python_implementation() == "PyPy", "GC set_debug not available in PyPy") class TestReferenceCleanup(DRYTest): """Test cleanup without cyclic references""" + # pylint: disable=attribute-defined-outside-init + def setUp(self): self._initial_gc_isenabled = gc.isenabled() @@ -1165,5 +1263,35 @@ simpleeval.SimpleEval() +class TestNoEntries(DRYTest): + def test_no_functions(self): + self.s.eval("int(42)") + with self.assertRaises(FunctionNotDefined): + s = SimpleEval(functions={}) + s.eval("int(42)") + + def test_no_names(self): + # does not work on current Py3, True et al. are keywords now + self.s.eval("True") + # with self.assertRaises(NameNotDefined): + s = SimpleEval(names={}) + if sys.version_info < (3,): + with self.assertRaises(NameNotDefined): + s.eval("True") + else: + s.eval("True") + + def test_no_operators(self): + self.s.eval("1+2") + self.s.eval("~2") + s = SimpleEval(operators={}) + + with self.assertRaises(OperatorNotDefined): + s.eval("1+2") + + with self.assertRaises(OperatorNotDefined): + s.eval("~ 2") + + if __name__ == "__main__": # pragma: no cover unittest.main()
