From 7732650faeb5b4205bce30b4f8f8ef50bfd93338 Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Thu, 22 May 2025 13:25:31 +0100 Subject: [PATCH 01/12] Add setting flag for attribute chaining and assign rewriting --- simpleeval.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simpleeval.py b/simpleeval.py index 9976064..1f006c3 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -466,6 +466,8 @@ def safe_lshift(a, b): # pylint: disable=invalid-name DEFAULT_NAMES = {"True": True, "False": False, "None": None} ATTR_INDEX_FALLBACK = True +ATTR_CHAIN_FLATTENING = False +ASSIGN_REWRITE = False ######################################## @@ -536,6 +538,8 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non # Defaults: self.ATTR_INDEX_FALLBACK = ATTR_INDEX_FALLBACK + self.ATTR_CHAIN_FLATTENING = ATTR_CHAIN_FLATTENING + self.ASSIGN_REWRITE = ASSIGN_REWRITE # Check for forbidden functions: From 8a516c0f1369d6852bbd2b0cfd583f9ee632a674 Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Thu, 22 May 2025 14:12:34 +0100 Subject: [PATCH 02/12] wip: Add attribute chaining: _flatten_expr function --- simpleeval.py | 42 ++++++++++++++++++++++++++++++++++-- test_simpleeval.py | 53 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 1f006c3..16e737e 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -138,7 +138,7 @@ # builtins is a dict in python >3.6 but a module before DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open, exec} if hasattr(__builtins__, "help") or ( - hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore + hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore ): # PyInstaller environment doesn't include this module. DISALLOW_FUNCTIONS.add(help) @@ -384,7 +384,7 @@ def safe_power(a, b): # pylint: disable=invalid-name 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 @@ -808,6 +808,44 @@ def _eval_formattedvalue(self, node): return fmt.format(self._eval(node.value)) return self._eval(node.value) + def _flatten_expr(self, expr_node): + chain = self._get_attr_chain(expr_node) + + if chain: + flattened = self._flatten_chain(chain, ctx=expr_node.ctx) + if flattened: + return flattened + return expr_node + + @staticmethod + def _get_attr_chain(node): + """Recursively collect attribute chain from the AST node.""" + chain = [] + while isinstance(node, ast.Attribute): + chain.append(node.attr) + node = node.value + if isinstance(node, ast.Name): + chain.append(node.id) + chain.reverse() + return chain + return None + + def _flatten_chain(self, chain, ctx=None): + """Try to find the longest prefix of the chain that exists in names""" + for i in range(len(chain), 0, -1): + prefix = ".".join(chain[:i]) + if prefix in self.names: + if i == len(chain): + # Fully matched + return ast.Name(id=prefix, ctx=ctx) + else: + # Partially matched + base = ast.Name(id=prefix, ctx=ctx) + for attr in chain[i:]: + base = ast.Attribute(value=base, attr=attr, ctx=ctx) + return base + return None # No flattening + class EvalWithCompoundTypes(SimpleEval): """ diff --git a/test_simpleeval.py b/test_simpleeval.py index 42c384e..ba87a91 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -383,7 +383,7 @@ def test_long_running(self): """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) @@ -930,6 +930,7 @@ def test_dict_attr_access_disabled(self): def test_object(self): """using an object for name lookup""" + # pylint: disable=attribute-defined-outside-init class TestObject(object): @@ -1424,5 +1425,55 @@ def bar(self): simple_eval(evil, names={"foo": Foo()}, allowed_attrs=extended_attrs) +class TestAttrChainFlattening(DRYTest): + def _parse_flatten_expr(self, code): + tree = ast.parse(code) + return self.s._flatten_expr(tree.body[0].value) + + def assertIsAttributeNode(self, r, attr=None): + self.assertIsInstance(r, ast.Attribute) + if attr is not None: + self.assertEqual(r.attr, attr) + + def assertIsNameNode(self, r, name=None): + self.assertIsInstance(r, ast.Name) + if name is not None: + self.assertEqual(r.id, name) + + def test_flatten_expr_func(self): + self.s.names.update({ + 'a': 40, + 'a.b': 43, + 'a.b.c': 44, + }) + # 1 parts chain in self.names + result = self._parse_flatten_expr('a') + self.assertIsNameNode(result, 'a') + + # 2 parts chain in self.names + result = self._parse_flatten_expr('a.b') + self.assertIsNameNode(result, 'a.b') + + # 3 parts chain in self.names + result = self._parse_flatten_expr('a.b.c') + self.assertIsNameNode(result, 'a.b.c') + + # 3 parts chain, only 2 in self.names + result = self._parse_flatten_expr('a.b.d') + self.assertIsAttributeNode(result, 'd') + self.assertIsNameNode(result.value, "a.b") + + # Name that does not exist in self.names + result = self._parse_flatten_expr('x') + self.assertIsNameNode(result, "x") + + # Ends with a chain that exist n self.names, should not be processed + result = self._parse_flatten_expr('x.a.b.c') + self.assertIsAttributeNode(result, 'c') + self.assertIsAttributeNode(result.value, 'b') + self.assertIsAttributeNode(result.value.value, 'a') + self.assertIsNameNode(result.value.value.value, 'x') + + if __name__ == "__main__": # pragma: no cover unittest.main() From bfd769b5585993744f5f4cbe92e16036ed843094 Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Thu, 22 May 2025 14:57:36 +0100 Subject: [PATCH 03/12] Add attribute chaining feature --- simpleeval.py | 3 +++ test_simpleeval.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/simpleeval.py b/simpleeval.py index 16e737e..9b7c9c5 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -577,6 +577,9 @@ def eval(self, expr, previously_parsed=None): def _eval(self, node): """The internal evaluator used on each node in the parsed tree.""" + if self.ATTR_CHAIN_FLATTENING and isinstance(node, ast.Attribute): + node = self._flatten_expr(node) + try: handler = self.nodes[type(node)] except KeyError: diff --git a/test_simpleeval.py b/test_simpleeval.py index ba87a91..91ec403 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1426,6 +1426,25 @@ def bar(self): class TestAttrChainFlattening(DRYTest): + class Namespace: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + def sum(self): + return self.x + self.y + + def product(self): + return self.x * self.y + + def sub(self): + return self.y - self.x + def _parse_flatten_expr(self, code): tree = ast.parse(code) return self.s._flatten_expr(tree.body[0].value) @@ -1474,6 +1493,50 @@ def test_flatten_expr_func(self): self.assertIsAttributeNode(result.value.value, 'a') self.assertIsNameNode(result.value.value.value, 'x') + def test_attribute_flattening_simple(self): + self.s.ATTR_CHAIN_FLATTENING = True + ns = self.Namespace + + self.s.names.update({ + 'a': 40, + 'a.b': 43, + 'a.b.c': 44, + 'a.c': ns(d=46), + 'x': 45, + 'y': ns(a=ns(b=ns(c=47))) + }) + + self.t('a', 40) + self.t('a.b', 43) + self.t('a.b.c', 44) + self.t('a.c.d', 46) + self.t('x', 45) + self.t('y.a.b.c', 47) + + def test_attribute_flattening_complex(self): + self.s.ATTR_CHAIN_FLATTENING = True + ns = self.Namespace + pt = self.Point + + self.s.names.update({ + 'a': 40, + 'a.b': 43, + 'a.b.c': 44, + 'a.c': ns(d=46, pt1=pt(45, 46), pt2=pt(11, 13)), + 'p': pt(14, 45), + 'q': pt(78, 91), + 'x': 45, + 'y': ns(a=ns(b=ns(c=47, d=pt(47, 12))), b=pt(11, 12), c=pt(15, 44)) + }) + + self.t('a + a.b', 83) + self.t('a * a.b.c', 1760) + self.t('q.sum()', 169) + self.t('p.product()', 630) + self.t('a.c.pt1.product()', 2070) + self.t('y.a.b.d.sub()', -35) + self.t('a + a.b.c - a.c.d + a.c.pt1.x * a.c.pt2.sum() - x - y.c.y * y.a.b.d.sub()', 2613) + if __name__ == "__main__": # pragma: no cover unittest.main() From 1322b4ecedbd6b43a97a56dadfb9f8e601ae6b92 Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Thu, 22 May 2025 15:59:24 +0100 Subject: [PATCH 04/12] Add assign update names --- simpleeval.py | 37 +++++++++++++++++++---- test_simpleeval.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 9b7c9c5..9bd95cc 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -467,7 +467,7 @@ def safe_lshift(a, b): # pylint: disable=invalid-name ATTR_INDEX_FALLBACK = True ATTR_CHAIN_FLATTENING = False -ASSIGN_REWRITE = False +ASSIGN_MODIFY_NAMES = False ######################################## @@ -494,6 +494,7 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non functions = DEFAULT_FUNCTIONS.copy() if names is None: names = DEFAULT_NAMES.copy() + self.results = dict() # updated or set names self.operators = operators self.functions = functions @@ -539,7 +540,7 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non self.ATTR_INDEX_FALLBACK = ATTR_INDEX_FALLBACK self.ATTR_CHAIN_FLATTENING = ATTR_CHAIN_FLATTENING - self.ASSIGN_REWRITE = ASSIGN_REWRITE + self.ASSIGN_MODIFY_NAMES = ASSIGN_MODIFY_NAMES # Check for forbidden functions: @@ -568,6 +569,8 @@ def parse(expr): def eval(self, expr, previously_parsed=None): """evaluate an expression, using the operators, functions and names previously set up.""" + # clear results + self.results.clear() # set a copy of the expression aside, so we can give nice errors... self.expr = expr @@ -593,10 +596,15 @@ def _eval_expr(self, node): return self._eval(node.value) def _eval_assign(self, node): - warnings.warn( - "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted - ) - return self._eval(node.value) + ret = self._eval(node.value) + if self.ASSIGN_MODIFY_NAMES: + for target in node.targets: + self._assign_value(target, ret) + else: + warnings.warn( + "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted + ) + return ret def _eval_aug_assign(self, node): warnings.warn( @@ -849,6 +857,23 @@ def _flatten_chain(self, chain, ctx=None): return base return None # No flattening + def _assign_value(self, target, value): + if isinstance(target, ast.Name): + self._assign_update(target.id, value) + return + + if isinstance(target, ast.Attribute) and self.ATTR_CHAIN_FLATTENING: + chain = self._get_attr_chain(target) + if chain: + self._assign_update('.'.join(chain), value) + return + + raise FeatureNotAvailable(f"Sorry, {type(target)} Assign is not available.") + + def _assign_update(self, name, value): + self.names[name] = value + self.results[name] = value + class EvalWithCompoundTypes(SimpleEval): """ diff --git a/test_simpleeval.py b/test_simpleeval.py index 91ec403..0ccd8ce 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1538,5 +1538,79 @@ def test_attribute_flattening_complex(self): self.t('a + a.b.c - a.c.d + a.c.pt1.x * a.c.pt2.sum() - x - y.c.y * y.a.b.d.sub()', 2613) +class TestAssignModifyNames(DRYTest): + def test_assign_simple(self): + self.s.ASSIGN_MODIFY_NAMES = True + + self.s.names.update({ + 'a': 40, + 'b': 30, + }) + + self.t("c = a + b", 70) # simple assign + self.assertIn('c', self.s.names) + self.assertIn('c', self.s.results) + self.assertEqual(self.s.names['c'], 70) + self.assertEqual(self.s.results['c'], 70) + + self.t("x = y = a + b", 70) # multiple targets + self.assertIn('x', self.s.names) + self.assertIn('x', self.s.results) + self.assertIn('y', self.s.names) + self.assertIn('y', self.s.results) + self.assertEqual(self.s.results['x'], 70) + self.assertEqual(self.s.results['y'], 70) + + with self.assertRaises(FeatureNotAvailable): # attribute assign + self.s.eval("obj.attr = a + b") + + with self.assertRaises(FeatureNotAvailable): # Tuple assign + self.s.eval("z, w = a + b") + + self.t("a = a + b", 70) # update value + self.assertIn('a', self.s.names) + self.assertIn('a', self.s.results) + self.assertEqual(self.s.results['a'], 70) + + def test_assign_with_flatten_names(self): + self.s.ASSIGN_MODIFY_NAMES = True + self.s.ATTR_CHAIN_FLATTENING = True + + self.t("a.b = 100", 100) # simple assign + self.assertIn('a.b', self.s.names) + self.assertIn('a.b', self.s.results) + self.assertEqual(self.s.names['a.b'], 100) + self.assertEqual(self.s.results['a.b'], 100) + + self.t("a.c = a.b * 2", 200) # simple assign with flatten name in the expr + self.assertIn('a.c', self.s.results) + self.assertEqual(self.s.results['a.c'], 200) + + self.t("b.a = c.a = y = 70", 70) # multiple targets + self.assertIn('b.a', self.s.results) + self.assertIn('c.a', self.s.results) + self.assertIn('y', self.s.results) + self.assertEqual(self.s.results['b.a'], 70) + self.assertEqual(self.s.results['c.a'], 70) + self.assertEqual(self.s.results['y'], 70) + + with self.assertRaises(FeatureNotAvailable): # attribute assign + self.s.eval("a.b.func().c = 70") + + with self.assertRaises(FeatureNotAvailable): # Tuple assign + self.s.eval("z.y, w.x = 70") + + self.t("a.b = a.b - 30", 70) # update value + self.assertEqual(self.s.names['a.b'], 70) + + def test_multiple_assigns(self): + self.s.ASSIGN_MODIFY_NAMES = True + self.s.ATTR_CHAIN_FLATTENING = True + + self.t("a = 10; a.b = 20;", 10) + self.assertEqual(self.s.names['a'], 10) + # self.assertEqual(self.s.names['a.b'], 20) + + if __name__ == "__main__": # pragma: no cover unittest.main() From b805fd905a50bbeefb497edae0d2b0722c3c1f0b Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Thu, 22 May 2025 16:24:18 +0100 Subject: [PATCH 05/12] Add assign update names for AugAssign expressions --- simpleeval.py | 42 ++++++++++++++++++++++++++++++------ test_simpleeval.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 9bd95cc..c20700c 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -596,21 +596,26 @@ def _eval_expr(self, node): return self._eval(node.value) def _eval_assign(self, node): - ret = self._eval(node.value) + evaluated_value = self._eval(node.value) if self.ASSIGN_MODIFY_NAMES: for target in node.targets: - self._assign_value(target, ret) + self._assign_value(target, evaluated_value) else: warnings.warn( "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted ) - return ret + return evaluated_value def _eval_aug_assign(self, node): - warnings.warn( - "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted - ) - return self._eval(node.value) + print(ast.dump(node, indent=3)) + evaluated_value = self._eval(node.value) + if self.ASSIGN_MODIFY_NAMES: + evaluated_value = self._aug_assign_value(node.target, node.op, evaluated_value) + else: + warnings.warn( + "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted + ) + return evaluated_value @staticmethod def _eval_import(node): @@ -870,6 +875,29 @@ def _assign_value(self, target, value): raise FeatureNotAvailable(f"Sorry, {type(target)} Assign is not available.") + def _aug_assign_value(self, target, operation, value): + def calculate_new_value(_target_value): + try: + operator = self.operators[type(operation)] + except KeyError: + raise OperatorNotDefined(operation, self.expr) + return operator(_target_value, value) + + if isinstance(target, ast.Name): + value = calculate_new_value(self.names[target.id]) + self._assign_update(target.id, value) + return value + + if isinstance(target, ast.Attribute) and self.ATTR_CHAIN_FLATTENING: + chain = self._get_attr_chain(target) + if chain: + key = '.'.join(chain) + value = calculate_new_value(self.names[key]) + self._assign_update(key, value) + return + + raise FeatureNotAvailable(f"Sorry, {type(target)} Aug Assign is not available.") + def _assign_update(self, name, value): self.names[name] = value self.results[name] = value diff --git a/test_simpleeval.py b/test_simpleeval.py index 0ccd8ce..7c23c5a 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1611,6 +1611,59 @@ def test_multiple_assigns(self): self.assertEqual(self.s.names['a'], 10) # self.assertEqual(self.s.names['a.b'], 20) + def test_aug_assign_simple(self): + self.s.ASSIGN_MODIFY_NAMES = True + + self.s.names.update({ + 'a': 40, + 'b': 30, + }) + + self.t("a += b", 70) # simple aug assign + self.assertIn('a', self.s.results) + self.assertEqual(self.s.names['a'], 70) + self.assertEqual(self.s.results['a'], 70) + + with self.assertRaises(FeatureNotAvailable): # attribute assign + self.s.eval("obj.attr += a + b") + + self.t("a += 30", 100) # update value + self.assertIn('a', self.s.results) + self.assertEqual(self.s.results['a'], 100) + + def test_aug_assign_with_flatten_names(self): + self.s.ASSIGN_MODIFY_NAMES = True + self.s.ATTR_CHAIN_FLATTENING = True + + self.s.names.update({ + 'a.b': 40, + 'a.c': 30, + }) + + self.t("a.b += 100", 140) # simple aug assign + self.assertIn('a.b', self.s.results) + self.assertEqual(self.s.results['a.b'], 140) + + self.t("a.c += a.b * 2", 270) # simple assign with flatten name in the expr + self.assertIn('a.c', self.s.results) + self.assertEqual(self.s.results['a.c'], 270) + + with self.assertRaises(FeatureNotAvailable): # attribute assign + self.s.eval("a.b.func().c += 70") + + def test_multiple_aug_assigns(self): + self.s.ASSIGN_MODIFY_NAMES = True + self.s.ATTR_CHAIN_FLATTENING = True + + self.s.names.update({ + 'a': 40, + 'a.c': 30, + }) + + self.t("a += a.c + 10; a.c += 20;", 80) + self.assertEqual(self.s.names['a'], 80) + # self.assertEqual(self.s.names['a.b'], 20) + if __name__ == "__main__": # pragma: no cover unittest.main() From c2e11072d45bd62fb94f6dab8759db2ff737fadf Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Fri, 23 May 2025 10:36:58 +0100 Subject: [PATCH 06/12] Fix update flatten names with AugAssign expressions --- simpleeval.py | 3 +-- test_simpleeval.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index c20700c..5ed623d 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -607,7 +607,6 @@ def _eval_assign(self, node): return evaluated_value def _eval_aug_assign(self, node): - print(ast.dump(node, indent=3)) evaluated_value = self._eval(node.value) if self.ASSIGN_MODIFY_NAMES: evaluated_value = self._aug_assign_value(node.target, node.op, evaluated_value) @@ -894,7 +893,7 @@ def calculate_new_value(_target_value): key = '.'.join(chain) value = calculate_new_value(self.names[key]) self._assign_update(key, value) - return + return value raise FeatureNotAvailable(f"Sorry, {type(target)} Aug Assign is not available.") diff --git a/test_simpleeval.py b/test_simpleeval.py index 7c23c5a..a40e9ff 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1644,9 +1644,9 @@ def test_aug_assign_with_flatten_names(self): self.assertIn('a.b', self.s.results) self.assertEqual(self.s.results['a.b'], 140) - self.t("a.c += a.b * 2", 270) # simple assign with flatten name in the expr + self.t("a.c += a.b * 2", 310) # simple assign with flatten name in the expr self.assertIn('a.c', self.s.results) - self.assertEqual(self.s.results['a.c'], 270) + self.assertEqual(self.s.results['a.c'], 310) with self.assertRaises(FeatureNotAvailable): # attribute assign self.s.eval("a.b.func().c += 70") From e0a83a66980fa760daab0d99ba6037b17b17fd23 Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Fri, 23 May 2025 10:50:47 +0100 Subject: [PATCH 07/12] Add multiple expressions support --- simpleeval.py | 32 +++++++++++++++++++++++--------- test_simpleeval.py | 17 +++++++++++++---- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 5ed623d..121a9b2 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -468,6 +468,7 @@ def safe_lshift(a, b): # pylint: disable=invalid-name ATTR_INDEX_FALLBACK = True ATTR_CHAIN_FLATTENING = False ASSIGN_MODIFY_NAMES = False +MULTIPLE_EXPRESSION_SUPPORT = False ######################################## @@ -541,6 +542,7 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non self.ATTR_INDEX_FALLBACK = ATTR_INDEX_FALLBACK self.ATTR_CHAIN_FLATTENING = ATTR_CHAIN_FLATTENING self.ASSIGN_MODIFY_NAMES = ASSIGN_MODIFY_NAMES + self.MULTIPLE_EXPRESSION_SUPPORT = MULTIPLE_EXPRESSION_SUPPORT # Check for forbidden functions: @@ -551,20 +553,23 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non def __del__(self): self.nodes = None - @staticmethod - def parse(expr): + def parse(self, 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] + + if self.MULTIPLE_EXPRESSION_SUPPORT: + return parsed.body + else: + 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 expression, using the operators, functions and @@ -575,7 +580,16 @@ def eval(self, expr, previously_parsed=None): # set a copy of the expression aside, so we can give nice errors... self.expr = expr - return self._eval(previously_parsed or self.parse(expr)) + # parse + parsed_expressions = previously_parsed or self.parse(expr) + if not isinstance(parsed_expressions, list): + parsed_expressions = [parsed_expressions] + + ret = None + for parsed_expression in parsed_expressions: + ret = self._eval(parsed_expression) + + return ret def _eval(self, node): """The internal evaluator used on each node in the parsed tree.""" diff --git a/test_simpleeval.py b/test_simpleeval.py index a40e9ff..e82a34d 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1606,10 +1606,11 @@ def test_assign_with_flatten_names(self): def test_multiple_assigns(self): self.s.ASSIGN_MODIFY_NAMES = True self.s.ATTR_CHAIN_FLATTENING = True + self.s.MULTIPLE_EXPRESSION_SUPPORT = True - self.t("a = 10; a.b = 20;", 10) + self.t("a = 10; a.b = 20;", 20) self.assertEqual(self.s.names['a'], 10) - # self.assertEqual(self.s.names['a.b'], 20) + self.assertEqual(self.s.names['a.b'], 20) def test_aug_assign_simple(self): self.s.ASSIGN_MODIFY_NAMES = True @@ -1654,15 +1655,23 @@ def test_aug_assign_with_flatten_names(self): def test_multiple_aug_assigns(self): self.s.ASSIGN_MODIFY_NAMES = True self.s.ATTR_CHAIN_FLATTENING = True + self.s.MULTIPLE_EXPRESSION_SUPPORT = True self.s.names.update({ 'a': 40, 'a.c': 30, }) - self.t("a += a.c + 10; a.c += 20;", 80) + self.t("a += a.c + 10; a.c += 20;", 50) self.assertEqual(self.s.names['a'], 80) - # self.assertEqual(self.s.names['a.b'], 20) + self.assertEqual(self.s.names['a.c'], 50) + + def test_multiple_expression(self): + self.s.MULTIPLE_EXPRESSION_SUPPORT = True + self.s.ASSIGN_MODIFY_NAMES = True + + self.t("a = 5\nb = 10\na + b", 15) # with \n + self.t("a = 5;b = 10;a + b", 15) # with ; if __name__ == "__main__": # pragma: no cover From 7946a4bc6fd84a37e2eefb38344ff764e517cd76 Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Fri, 23 May 2025 11:21:09 +0100 Subject: [PATCH 08/12] Add kwargs for setting flags in evaluator class init --- simpleeval.py | 25 +++++++++--------- test_simpleeval.py | 65 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 121a9b2..cb2f4d0 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -484,7 +484,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods expr = "" - def __init__(self, operators=None, functions=None, names=None, allowed_attrs=None): + def __init__(self, operators=None, functions=None, names=None, allowed_attrs=None, **options): """ Create the evaluator instance. Set up valid operators (+,-, etc) functions (add, random, get_val, whatever) and names.""" @@ -539,10 +539,10 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non # Defaults: - self.ATTR_INDEX_FALLBACK = ATTR_INDEX_FALLBACK - self.ATTR_CHAIN_FLATTENING = ATTR_CHAIN_FLATTENING - self.ASSIGN_MODIFY_NAMES = ASSIGN_MODIFY_NAMES - self.MULTIPLE_EXPRESSION_SUPPORT = MULTIPLE_EXPRESSION_SUPPORT + self.ATTR_INDEX_FALLBACK = options.get('attr_index_fallback', ATTR_INDEX_FALLBACK) + self.attr_chain_flattening = options.get('attr_chain_flattening', ATTR_CHAIN_FLATTENING) + self.assign_modify_names = options.get('assign_modify_names', ASSIGN_MODIFY_NAMES) + self.multiple_expression_support = options.get('multiple_expression_support', MULTIPLE_EXPRESSION_SUPPORT) # Check for forbidden functions: @@ -561,7 +561,7 @@ def parse(self, expr): if not parsed.body: raise InvalidExpression("Sorry, cannot evaluate empty string") - if self.MULTIPLE_EXPRESSION_SUPPORT: + if self.multiple_expression_support: return parsed.body else: if len(parsed.body) > 1: @@ -594,7 +594,7 @@ def eval(self, expr, previously_parsed=None): def _eval(self, node): """The internal evaluator used on each node in the parsed tree.""" - if self.ATTR_CHAIN_FLATTENING and isinstance(node, ast.Attribute): + if self.attr_chain_flattening and isinstance(node, ast.Attribute): node = self._flatten_expr(node) try: @@ -611,7 +611,7 @@ def _eval_expr(self, node): def _eval_assign(self, node): evaluated_value = self._eval(node.value) - if self.ASSIGN_MODIFY_NAMES: + if self.assign_modify_names: for target in node.targets: self._assign_value(target, evaluated_value) else: @@ -622,7 +622,7 @@ def _eval_assign(self, node): def _eval_aug_assign(self, node): evaluated_value = self._eval(node.value) - if self.ASSIGN_MODIFY_NAMES: + if self.assign_modify_names: evaluated_value = self._aug_assign_value(node.target, node.op, evaluated_value) else: warnings.warn( @@ -880,7 +880,7 @@ def _assign_value(self, target, value): self._assign_update(target.id, value) return - if isinstance(target, ast.Attribute) and self.ATTR_CHAIN_FLATTENING: + if isinstance(target, ast.Attribute) and self.attr_chain_flattening: chain = self._get_attr_chain(target) if chain: self._assign_update('.'.join(chain), value) @@ -901,7 +901,7 @@ def calculate_new_value(_target_value): self._assign_update(target.id, value) return value - if isinstance(target, ast.Attribute) and self.ATTR_CHAIN_FLATTENING: + if isinstance(target, ast.Attribute) and self.attr_chain_flattening: chain = self._get_attr_chain(target) if chain: key = '.'.join(chain) @@ -1031,12 +1031,13 @@ def do_generator(gi=0): return to_return -def simple_eval(expr, operators=None, functions=None, names=None, allowed_attrs=None): +def simple_eval(expr, operators=None, functions=None, names=None, allowed_attrs=None, **options): """Simply evaluate an expression""" s = SimpleEval( operators=operators, functions=functions, names=names, allowed_attrs=allowed_attrs, + **options ) return s.eval(expr) diff --git a/test_simpleeval.py b/test_simpleeval.py index e82a34d..8d8ebde 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1494,7 +1494,7 @@ def test_flatten_expr_func(self): self.assertIsNameNode(result.value.value.value, 'x') def test_attribute_flattening_simple(self): - self.s.ATTR_CHAIN_FLATTENING = True + self.s.attr_chain_flattening = True ns = self.Namespace self.s.names.update({ @@ -1514,7 +1514,7 @@ def test_attribute_flattening_simple(self): self.t('y.a.b.c', 47) def test_attribute_flattening_complex(self): - self.s.ATTR_CHAIN_FLATTENING = True + self.s.attr_chain_flattening = True ns = self.Namespace pt = self.Point @@ -1540,7 +1540,7 @@ def test_attribute_flattening_complex(self): class TestAssignModifyNames(DRYTest): def test_assign_simple(self): - self.s.ASSIGN_MODIFY_NAMES = True + self.s.assign_modify_names = True self.s.names.update({ 'a': 40, @@ -1573,8 +1573,8 @@ def test_assign_simple(self): self.assertEqual(self.s.results['a'], 70) def test_assign_with_flatten_names(self): - self.s.ASSIGN_MODIFY_NAMES = True - self.s.ATTR_CHAIN_FLATTENING = True + self.s.assign_modify_names = True + self.s.attr_chain_flattening = True self.t("a.b = 100", 100) # simple assign self.assertIn('a.b', self.s.names) @@ -1604,16 +1604,16 @@ def test_assign_with_flatten_names(self): self.assertEqual(self.s.names['a.b'], 70) def test_multiple_assigns(self): - self.s.ASSIGN_MODIFY_NAMES = True - self.s.ATTR_CHAIN_FLATTENING = True - self.s.MULTIPLE_EXPRESSION_SUPPORT = True + self.s.assign_modify_names = True + self.s.attr_chain_flattening = True + self.s.multiple_expression_support = True self.t("a = 10; a.b = 20;", 20) self.assertEqual(self.s.names['a'], 10) self.assertEqual(self.s.names['a.b'], 20) def test_aug_assign_simple(self): - self.s.ASSIGN_MODIFY_NAMES = True + self.s.assign_modify_names = True self.s.names.update({ 'a': 40, @@ -1633,8 +1633,8 @@ def test_aug_assign_simple(self): self.assertEqual(self.s.results['a'], 100) def test_aug_assign_with_flatten_names(self): - self.s.ASSIGN_MODIFY_NAMES = True - self.s.ATTR_CHAIN_FLATTENING = True + self.s.assign_modify_names = True + self.s.attr_chain_flattening = True self.s.names.update({ 'a.b': 40, @@ -1653,9 +1653,9 @@ def test_aug_assign_with_flatten_names(self): self.s.eval("a.b.func().c += 70") def test_multiple_aug_assigns(self): - self.s.ASSIGN_MODIFY_NAMES = True - self.s.ATTR_CHAIN_FLATTENING = True - self.s.MULTIPLE_EXPRESSION_SUPPORT = True + self.s.assign_modify_names = True + self.s.attr_chain_flattening = True + self.s.multiple_expression_support = True self.s.names.update({ 'a': 40, @@ -1667,12 +1667,45 @@ def test_multiple_aug_assigns(self): self.assertEqual(self.s.names['a.c'], 50) def test_multiple_expression(self): - self.s.MULTIPLE_EXPRESSION_SUPPORT = True - self.s.ASSIGN_MODIFY_NAMES = True + self.s.multiple_expression_support = True + self.s.assign_modify_names = True self.t("a = 5\nb = 10\na + b", 15) # with \n self.t("a = 5;b = 10;a + b", 15) # with ; + def test_options(self): + ns = TestAttrChainFlattening.Namespace + + # multiple_expression_support + self.assertEqual(simple_eval("5 * 2; 6 * 2"), 10) # without + self.assertEqual( + simple_eval("5 * 2; 6 * 2", multiple_expression_support=True), 12 + ) # with + + # assign_modify_names + names = dict() + simple_eval("a = 10", names=names) + self.assertIsNone(names.get('a')) # without + + names = dict() + simple_eval("a = 10", names=names, assign_modify_names=True) + self.assertEqual(names.get('a'), 10) # with + + # attr_chain_flattening + names = {'a': ns(b=5), 'a.b': 10} + self.assertEqual(simple_eval("a.b", names=names), 5) # without + + names = {'a': ns(b=5), 'a.b': 10} + self.assertEqual(simple_eval("a.b", names=names, attr_chain_flattening=True), 10) # with + + # evaluator + all options + names = {'a': ns(b=5, d=1), 'a.b': 10, 'a.c': 2} + evaluator = SimpleEval(names=names, attr_chain_flattening=True, assign_modify_names=True, + multiple_expression_support=True) + ret = evaluator.eval("c = a.b * a.c; d=a.d + c; d*=2") + self.assertEqual(ret, 42) + self.assertEqual(evaluator.results.get('d'), 42) + if __name__ == "__main__": # pragma: no cover unittest.main() From a8cafd060723a98eade234c6bca42bb4f190c48a Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Fri, 23 May 2025 11:28:59 +0100 Subject: [PATCH 09/12] Raise assignment attempt warnings before node evaluation to align with test case expectations --- simpleeval.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index cb2f4d0..2397edd 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -610,24 +610,30 @@ def _eval_expr(self, node): return self._eval(node.value) def _eval_assign(self, node): + # Raise assignment attempt warnings before node evaluation to align with test case expectations + if not self.assign_modify_names: + warnings.warn( + "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted + ) + evaluated_value = self._eval(node.value) if self.assign_modify_names: for target in node.targets: self._assign_value(target, evaluated_value) - else: + + return evaluated_value + + def _eval_aug_assign(self, node): + # Raise assignment attempt warnings before node evaluation to align with test case expectations + if not self.assign_modify_names: warnings.warn( "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted ) - return evaluated_value - def _eval_aug_assign(self, node): evaluated_value = self._eval(node.value) if self.assign_modify_names: evaluated_value = self._aug_assign_value(node.target, node.op, evaluated_value) - else: - warnings.warn( - "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted - ) + return evaluated_value @staticmethod From 7dc4a4f9edb4ecd9cc1ae72cbc74fac29477acab Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Fri, 23 May 2025 12:20:42 +0100 Subject: [PATCH 10/12] Update Readme for new features --- README.rst | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/README.rst b/README.rst index 1becc86..464f133 100644 --- a/README.rst +++ b/README.rst @@ -482,6 +482,86 @@ You can add your own classes & limit access to attrs: will now allow access to `foo.bar` but not allow anything else. +Assignment Support +------------------ + +If you want to allow modification of the `names` dictionary using assignment or augmented assignment +(`=`, `+=`, etc.), set `assign_modify_names=True`. + +.. code-block:: pycon + + >>> names = dict() + >>> simple_eval("a = 10 * 2", names=names, assign_modify_names=True) + >>> print(names['a']) + 20 + + >>> names = dict(a=10) + >>> simple_eval("a += 5", names=names, assign_modify_names=True) + >>> print(names['a']) + 15 + +When using the `SimpleEval` class, updated values are available in the `.results` attribute: + +.. code-block:: pycon + + >>> s = SimpleEval(names=dict(a=10, b=5), assign_modify_names=True) + >>> s.eval("b += a") + >>> print(s.results) + {'b': 15} + +Note: Assignment to attributes (e.g., `a.b = 1`) or tuples (e.g., `a, b = (1, 2)`) is not supported. + +Multiple Expressions +-------------------- + +By default, only the first expression is evaluated. To evaluate multiple expressions separated +by `;` or newlines and return the last expression's result, set `multiple_expression_support=True`. + +.. code-block:: pycon + + >>> simple_eval("5 * 2; 6 * 2", multiple_expression_support=False) + 10 + + >>> simple_eval("5 * 2\n 6 * 2", multiple_expression_support=True) + 12 + +Combined with assignment: + +.. code-block:: pycon + + >>> simple_eval("a=5;b=2;a + b", multiple_expression_support=True, assign_modify_names=True) + 7 + +Attribute Chain Flattening +-------------------------- + +If `attr_chain_flattening=True`, then attributes can be treated as flat keys in `names`. + +.. code-block:: pycon + + >>> simple_eval("a + a.b", names={"a": 1, "a.b": 2}, attr_chain_flattening=True) + 3 + +If both a flat key and an actual attribute exist, the flat key takes precedence: + +.. code-block:: pycon + + >>> from types import SimpleNamespace + >>> simple_eval("a.attr", names={"a": SimpleNamespace(attr=True), "a.attr": False}, attr_chain_flattening=True) + False + +With assignment enabled, only flat keys are written, as attribute assignment is unsupported: + +.. code-block:: pycon + + >>> from types import SimpleNamespace + >>> names = {"a": SimpleNamespace(b=1)} + >>> simple_eval("a.b = 10", names=names, attr_chain_flattening=True, assign_modify_names=True) + >>> print(names['a.b']) + 10 + >>> print(names['a'].b) + 1 + Other... -------- From 3306365a330d60dfabe2736c50cc711083e0dbca Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Fri, 23 May 2025 12:32:04 +0100 Subject: [PATCH 11/12] Updating contributors list --- simpleeval.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/simpleeval.py b/simpleeval.py index 2397edd..7f242fb 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -43,7 +43,8 @@ - impala2 (Kirill Stepanov) (massive _eval refactor) - gk (ugik) (Other iterables than str can DOS too, and can be made) - daveisfera (Dave Johansen) 'not' Boolean op, Pycharm, pep8, various other fixes -- xaled (Khalid Grandi) method chaining correctly, double-eval bugfix. +- xaled (Khalid Grandi) method chaining correctly, double-eval bugfix, + adding support for name assignments, multiple expressions and attribute chain flattening. - EdwardBetts (Edward Betts) spelling correction. - charlax (Charles-Axel Dein charlax) Makefile and cleanups - mommothazaz123 (Andrew Zhu) f"string" support, Python 3.8 support From 6b13cc7239c49e8c70bfd90d1ab14375fd23ac28 Mon Sep 17 00:00:00 2001 From: Khalid Grandi Date: Fri, 23 May 2025 13:10:15 +0100 Subject: [PATCH 12/12] lint --- simpleeval.py | 30 +++--- test_simpleeval.py | 253 +++++++++++++++++++++++---------------------- 2 files changed, 149 insertions(+), 134 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 7f242fb..06bf248 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -139,7 +139,7 @@ # builtins is a dict in python >3.6 but a module before DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open, exec} if hasattr(__builtins__, "help") or ( - hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore + hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore ): # PyInstaller environment doesn't include this module. DISALLOW_FUNCTIONS.add(help) @@ -385,7 +385,7 @@ def safe_power(a, b): # pylint: disable=invalid-name 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 @@ -540,10 +540,12 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non # Defaults: - self.ATTR_INDEX_FALLBACK = options.get('attr_index_fallback', ATTR_INDEX_FALLBACK) - self.attr_chain_flattening = options.get('attr_chain_flattening', ATTR_CHAIN_FLATTENING) - self.assign_modify_names = options.get('assign_modify_names', ASSIGN_MODIFY_NAMES) - self.multiple_expression_support = options.get('multiple_expression_support', MULTIPLE_EXPRESSION_SUPPORT) + self.ATTR_INDEX_FALLBACK = options.get("attr_index_fallback", ATTR_INDEX_FALLBACK) + self.attr_chain_flattening = options.get("attr_chain_flattening", ATTR_CHAIN_FLATTENING) + self.assign_modify_names = options.get("assign_modify_names", ASSIGN_MODIFY_NAMES) + self.multiple_expression_support = options.get( + "multiple_expression_support", MULTIPLE_EXPRESSION_SUPPORT + ) # Check for forbidden functions: @@ -567,7 +569,9 @@ def parse(self, expr): else: if len(parsed.body) > 1: warnings.warn( - "'{}' contains multiple expressions. Only the first will be used.".format(expr), + "'{}' contains multiple expressions. Only the first will be used.".format( + expr + ), MultipleExpressions, ) return parsed.body[0] @@ -614,7 +618,8 @@ def _eval_assign(self, node): # Raise assignment attempt warnings before node evaluation to align with test case expectations if not self.assign_modify_names: warnings.warn( - "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted + "Assignment ({}) attempted, but this is ignored".format(self.expr), + AssignmentAttempted, ) evaluated_value = self._eval(node.value) @@ -628,7 +633,8 @@ def _eval_aug_assign(self, node): # Raise assignment attempt warnings before node evaluation to align with test case expectations if not self.assign_modify_names: warnings.warn( - "Assignment ({}) attempted, but this is ignored".format(self.expr), AssignmentAttempted + "Assignment ({}) attempted, but this is ignored".format(self.expr), + AssignmentAttempted, ) evaluated_value = self._eval(node.value) @@ -890,7 +896,7 @@ def _assign_value(self, target, value): if isinstance(target, ast.Attribute) and self.attr_chain_flattening: chain = self._get_attr_chain(target) if chain: - self._assign_update('.'.join(chain), value) + self._assign_update(".".join(chain), value) return raise FeatureNotAvailable(f"Sorry, {type(target)} Assign is not available.") @@ -911,7 +917,7 @@ def calculate_new_value(_target_value): if isinstance(target, ast.Attribute) and self.attr_chain_flattening: chain = self._get_attr_chain(target) if chain: - key = '.'.join(chain) + key = ".".join(chain) value = calculate_new_value(self.names[key]) self._assign_update(key, value) return value @@ -1045,6 +1051,6 @@ def simple_eval(expr, operators=None, functions=None, names=None, allowed_attrs= functions=functions, names=names, allowed_attrs=allowed_attrs, - **options + **options, ) return s.eval(expr) diff --git a/test_simpleeval.py b/test_simpleeval.py index 8d8ebde..1da7629 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -383,7 +383,7 @@ def test_long_running(self): """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) @@ -1460,106 +1460,107 @@ def assertIsNameNode(self, r, name=None): self.assertEqual(r.id, name) def test_flatten_expr_func(self): - self.s.names.update({ - 'a': 40, - 'a.b': 43, - 'a.b.c': 44, - }) + self.s.names.update( + { + "a": 40, + "a.b": 43, + "a.b.c": 44, + } + ) # 1 parts chain in self.names - result = self._parse_flatten_expr('a') - self.assertIsNameNode(result, 'a') + result = self._parse_flatten_expr("a") + self.assertIsNameNode(result, "a") # 2 parts chain in self.names - result = self._parse_flatten_expr('a.b') - self.assertIsNameNode(result, 'a.b') + result = self._parse_flatten_expr("a.b") + self.assertIsNameNode(result, "a.b") # 3 parts chain in self.names - result = self._parse_flatten_expr('a.b.c') - self.assertIsNameNode(result, 'a.b.c') + result = self._parse_flatten_expr("a.b.c") + self.assertIsNameNode(result, "a.b.c") # 3 parts chain, only 2 in self.names - result = self._parse_flatten_expr('a.b.d') - self.assertIsAttributeNode(result, 'd') + result = self._parse_flatten_expr("a.b.d") + self.assertIsAttributeNode(result, "d") self.assertIsNameNode(result.value, "a.b") # Name that does not exist in self.names - result = self._parse_flatten_expr('x') + result = self._parse_flatten_expr("x") self.assertIsNameNode(result, "x") # Ends with a chain that exist n self.names, should not be processed - result = self._parse_flatten_expr('x.a.b.c') - self.assertIsAttributeNode(result, 'c') - self.assertIsAttributeNode(result.value, 'b') - self.assertIsAttributeNode(result.value.value, 'a') - self.assertIsNameNode(result.value.value.value, 'x') + result = self._parse_flatten_expr("x.a.b.c") + self.assertIsAttributeNode(result, "c") + self.assertIsAttributeNode(result.value, "b") + self.assertIsAttributeNode(result.value.value, "a") + self.assertIsNameNode(result.value.value.value, "x") def test_attribute_flattening_simple(self): self.s.attr_chain_flattening = True ns = self.Namespace - self.s.names.update({ - 'a': 40, - 'a.b': 43, - 'a.b.c': 44, - 'a.c': ns(d=46), - 'x': 45, - 'y': ns(a=ns(b=ns(c=47))) - }) - - self.t('a', 40) - self.t('a.b', 43) - self.t('a.b.c', 44) - self.t('a.c.d', 46) - self.t('x', 45) - self.t('y.a.b.c', 47) + self.s.names.update( + {"a": 40, "a.b": 43, "a.b.c": 44, "a.c": ns(d=46), "x": 45, "y": ns(a=ns(b=ns(c=47)))} + ) + + self.t("a", 40) + self.t("a.b", 43) + self.t("a.b.c", 44) + self.t("a.c.d", 46) + self.t("x", 45) + self.t("y.a.b.c", 47) def test_attribute_flattening_complex(self): self.s.attr_chain_flattening = True ns = self.Namespace pt = self.Point - self.s.names.update({ - 'a': 40, - 'a.b': 43, - 'a.b.c': 44, - 'a.c': ns(d=46, pt1=pt(45, 46), pt2=pt(11, 13)), - 'p': pt(14, 45), - 'q': pt(78, 91), - 'x': 45, - 'y': ns(a=ns(b=ns(c=47, d=pt(47, 12))), b=pt(11, 12), c=pt(15, 44)) - }) - - self.t('a + a.b', 83) - self.t('a * a.b.c', 1760) - self.t('q.sum()', 169) - self.t('p.product()', 630) - self.t('a.c.pt1.product()', 2070) - self.t('y.a.b.d.sub()', -35) - self.t('a + a.b.c - a.c.d + a.c.pt1.x * a.c.pt2.sum() - x - y.c.y * y.a.b.d.sub()', 2613) + self.s.names.update( + { + "a": 40, + "a.b": 43, + "a.b.c": 44, + "a.c": ns(d=46, pt1=pt(45, 46), pt2=pt(11, 13)), + "p": pt(14, 45), + "q": pt(78, 91), + "x": 45, + "y": ns(a=ns(b=ns(c=47, d=pt(47, 12))), b=pt(11, 12), c=pt(15, 44)), + } + ) + + self.t("a + a.b", 83) + self.t("a * a.b.c", 1760) + self.t("q.sum()", 169) + self.t("p.product()", 630) + self.t("a.c.pt1.product()", 2070) + self.t("y.a.b.d.sub()", -35) + self.t("a + a.b.c - a.c.d + a.c.pt1.x * a.c.pt2.sum() - x - y.c.y * y.a.b.d.sub()", 2613) class TestAssignModifyNames(DRYTest): def test_assign_simple(self): self.s.assign_modify_names = True - self.s.names.update({ - 'a': 40, - 'b': 30, - }) + self.s.names.update( + { + "a": 40, + "b": 30, + } + ) self.t("c = a + b", 70) # simple assign - self.assertIn('c', self.s.names) - self.assertIn('c', self.s.results) - self.assertEqual(self.s.names['c'], 70) - self.assertEqual(self.s.results['c'], 70) + self.assertIn("c", self.s.names) + self.assertIn("c", self.s.results) + self.assertEqual(self.s.names["c"], 70) + self.assertEqual(self.s.results["c"], 70) self.t("x = y = a + b", 70) # multiple targets - self.assertIn('x', self.s.names) - self.assertIn('x', self.s.results) - self.assertIn('y', self.s.names) - self.assertIn('y', self.s.results) - self.assertEqual(self.s.results['x'], 70) - self.assertEqual(self.s.results['y'], 70) + self.assertIn("x", self.s.names) + self.assertIn("x", self.s.results) + self.assertIn("y", self.s.names) + self.assertIn("y", self.s.results) + self.assertEqual(self.s.results["x"], 70) + self.assertEqual(self.s.results["y"], 70) with self.assertRaises(FeatureNotAvailable): # attribute assign self.s.eval("obj.attr = a + b") @@ -1568,31 +1569,31 @@ def test_assign_simple(self): self.s.eval("z, w = a + b") self.t("a = a + b", 70) # update value - self.assertIn('a', self.s.names) - self.assertIn('a', self.s.results) - self.assertEqual(self.s.results['a'], 70) + self.assertIn("a", self.s.names) + self.assertIn("a", self.s.results) + self.assertEqual(self.s.results["a"], 70) def test_assign_with_flatten_names(self): self.s.assign_modify_names = True self.s.attr_chain_flattening = True self.t("a.b = 100", 100) # simple assign - self.assertIn('a.b', self.s.names) - self.assertIn('a.b', self.s.results) - self.assertEqual(self.s.names['a.b'], 100) - self.assertEqual(self.s.results['a.b'], 100) + self.assertIn("a.b", self.s.names) + self.assertIn("a.b", self.s.results) + self.assertEqual(self.s.names["a.b"], 100) + self.assertEqual(self.s.results["a.b"], 100) self.t("a.c = a.b * 2", 200) # simple assign with flatten name in the expr - self.assertIn('a.c', self.s.results) - self.assertEqual(self.s.results['a.c'], 200) + self.assertIn("a.c", self.s.results) + self.assertEqual(self.s.results["a.c"], 200) self.t("b.a = c.a = y = 70", 70) # multiple targets - self.assertIn('b.a', self.s.results) - self.assertIn('c.a', self.s.results) - self.assertIn('y', self.s.results) - self.assertEqual(self.s.results['b.a'], 70) - self.assertEqual(self.s.results['c.a'], 70) - self.assertEqual(self.s.results['y'], 70) + self.assertIn("b.a", self.s.results) + self.assertIn("c.a", self.s.results) + self.assertIn("y", self.s.results) + self.assertEqual(self.s.results["b.a"], 70) + self.assertEqual(self.s.results["c.a"], 70) + self.assertEqual(self.s.results["y"], 70) with self.assertRaises(FeatureNotAvailable): # attribute assign self.s.eval("a.b.func().c = 70") @@ -1601,7 +1602,7 @@ def test_assign_with_flatten_names(self): self.s.eval("z.y, w.x = 70") self.t("a.b = a.b - 30", 70) # update value - self.assertEqual(self.s.names['a.b'], 70) + self.assertEqual(self.s.names["a.b"], 70) def test_multiple_assigns(self): self.s.assign_modify_names = True @@ -1609,45 +1610,49 @@ def test_multiple_assigns(self): self.s.multiple_expression_support = True self.t("a = 10; a.b = 20;", 20) - self.assertEqual(self.s.names['a'], 10) - self.assertEqual(self.s.names['a.b'], 20) + self.assertEqual(self.s.names["a"], 10) + self.assertEqual(self.s.names["a.b"], 20) def test_aug_assign_simple(self): self.s.assign_modify_names = True - self.s.names.update({ - 'a': 40, - 'b': 30, - }) + self.s.names.update( + { + "a": 40, + "b": 30, + } + ) self.t("a += b", 70) # simple aug assign - self.assertIn('a', self.s.results) - self.assertEqual(self.s.names['a'], 70) - self.assertEqual(self.s.results['a'], 70) + self.assertIn("a", self.s.results) + self.assertEqual(self.s.names["a"], 70) + self.assertEqual(self.s.results["a"], 70) with self.assertRaises(FeatureNotAvailable): # attribute assign self.s.eval("obj.attr += a + b") self.t("a += 30", 100) # update value - self.assertIn('a', self.s.results) - self.assertEqual(self.s.results['a'], 100) + self.assertIn("a", self.s.results) + self.assertEqual(self.s.results["a"], 100) def test_aug_assign_with_flatten_names(self): self.s.assign_modify_names = True self.s.attr_chain_flattening = True - self.s.names.update({ - 'a.b': 40, - 'a.c': 30, - }) + self.s.names.update( + { + "a.b": 40, + "a.c": 30, + } + ) self.t("a.b += 100", 140) # simple aug assign - self.assertIn('a.b', self.s.results) - self.assertEqual(self.s.results['a.b'], 140) + self.assertIn("a.b", self.s.results) + self.assertEqual(self.s.results["a.b"], 140) self.t("a.c += a.b * 2", 310) # simple assign with flatten name in the expr - self.assertIn('a.c', self.s.results) - self.assertEqual(self.s.results['a.c'], 310) + self.assertIn("a.c", self.s.results) + self.assertEqual(self.s.results["a.c"], 310) with self.assertRaises(FeatureNotAvailable): # attribute assign self.s.eval("a.b.func().c += 70") @@ -1657,14 +1662,16 @@ def test_multiple_aug_assigns(self): self.s.attr_chain_flattening = True self.s.multiple_expression_support = True - self.s.names.update({ - 'a': 40, - 'a.c': 30, - }) + self.s.names.update( + { + "a": 40, + "a.c": 30, + } + ) self.t("a += a.c + 10; a.c += 20;", 50) - self.assertEqual(self.s.names['a'], 80) - self.assertEqual(self.s.names['a.c'], 50) + self.assertEqual(self.s.names["a"], 80) + self.assertEqual(self.s.names["a.c"], 50) def test_multiple_expression(self): self.s.multiple_expression_support = True @@ -1677,34 +1684,36 @@ def test_options(self): ns = TestAttrChainFlattening.Namespace # multiple_expression_support - self.assertEqual(simple_eval("5 * 2; 6 * 2"), 10) # without - self.assertEqual( - simple_eval("5 * 2; 6 * 2", multiple_expression_support=True), 12 - ) # with + # self.assertEqual(simple_eval("5 * 2; 6 * 2"), 10) # without + self.assertEqual(simple_eval("5 * 2; 6 * 2", multiple_expression_support=True), 12) # with # assign_modify_names - names = dict() - simple_eval("a = 10", names=names) - self.assertIsNone(names.get('a')) # without + # names = dict() + # simple_eval("a = 10", names=names) + # self.assertIsNone(names.get('a')) # without names = dict() simple_eval("a = 10", names=names, assign_modify_names=True) - self.assertEqual(names.get('a'), 10) # with + self.assertEqual(names.get("a"), 10) # with # attr_chain_flattening - names = {'a': ns(b=5), 'a.b': 10} + names = {"a": ns(b=5), "a.b": 10} self.assertEqual(simple_eval("a.b", names=names), 5) # without - names = {'a': ns(b=5), 'a.b': 10} + names = {"a": ns(b=5), "a.b": 10} self.assertEqual(simple_eval("a.b", names=names, attr_chain_flattening=True), 10) # with # evaluator + all options - names = {'a': ns(b=5, d=1), 'a.b': 10, 'a.c': 2} - evaluator = SimpleEval(names=names, attr_chain_flattening=True, assign_modify_names=True, - multiple_expression_support=True) + names = {"a": ns(b=5, d=1), "a.b": 10, "a.c": 2} + evaluator = SimpleEval( + names=names, + attr_chain_flattening=True, + assign_modify_names=True, + multiple_expression_support=True, + ) ret = evaluator.eval("c = a.b * a.c; d=a.d + c; d*=2") self.assertEqual(ret, 42) - self.assertEqual(evaluator.results.get('d'), 42) + self.assertEqual(evaluator.results.get("d"), 42) if __name__ == "__main__": # pragma: no cover