diff --git a/README.rst b/README.rst index b0521f7..8192ee4 100644 --- a/README.rst +++ b/README.rst @@ -277,9 +277,34 @@ You can also hand the handling of names over to a function, if you prefer: 3 That was a bit of a silly example, but you could use this for pulling values -from a database or file, say, or doing some kind of caching system. +from a database or file, looking up spreadsheet cells, say, or doing some kind of caching system. -The two default names that are provided are ``True`` and ``False``. So if you want to provide your own names, but want ``True`` and ``False`` to keep working, either provide them yourself, or ``.copy()`` and ``.update`` the ``DEFAULT_NAMES``. (See functions example above). +In general, when it attempts to find a variable by name, if it cannot find one, +then it will look in the ``functions`` for a function of that name. If you want your name handler +function to return a "I can't find that name!", then it should raise a ``simpleeval.NameNotDefined`` +exception. Eg: + +.. code-block:: python + + >>> def name_handler(node): + ... if node.id[0] == 'a': + ... return 21 + ... raise NameNotDefined(node.id[0], "Not found") + ... + ... simple_eval('a + a', names=name_handler, functions={"b": 100}) + + 42 + + >>> simple_eval('a + b', names=name_handler, functions={'b': 100}) + 121 + +(Note: in that example, putting a number directly into the functions dict was done just to +show the fall-back to functions. Normally only put actual callables in there.) + + +The two default names that are provided are ``True`` and ``False``. So if you want to provide +your own names, but want ``True`` and ``False`` to keep working, either provide them yourself, +or ``.copy()`` and ``.update`` the ``DEFAULT_NAMES``. (See functions example above). Creating an Evaluator Class --------------------------- diff --git a/simpleeval.py b/simpleeval.py index d32d946..3b16cab 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -526,24 +526,30 @@ def _eval_name(self, node): try: # This happens at least for slicing # This is a safe thing to do because it is impossible - # that there is a true exression assigning to none + # that there is a true expression assigning to none # (the compiler rejects it, so you can't even # pass that to ast.parse) - if hasattr(self.names, "__getitem__"): - return self.names[node.id] - if callable(self.names): + return self.names[node.id] + + except (TypeError, KeyError): + pass + + if callable(self.names): + try: return self.names(node) + except NameNotDefined: + pass + elif not hasattr(self.names, "__getitem__"): 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: - return self.functions[node.id] + if node.id in self.functions: + return self.functions[node.id] - raise NameNotDefined(node.id, self.expr) + raise NameNotDefined(node.id, self.expr) def _eval_subscript(self, node): container = self._eval(node.value) diff --git a/test_simpleeval.py b/test_simpleeval.py index 1cc655f..bb430b2 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -933,6 +933,36 @@ def name_handler(node): self.t("a", 1) self.t("a + b", 3) + def test_name_handler_name_not_found(self): + def name_handler(node): + if node.id[0] == "a": + return 21 + raise NameNotDefined(node.id[0], "not found") + + self.s.names = name_handler + self.s.functions = {"b": lambda: 100} + self.t("a + a", 42) + + self.t("b()", 100) + + with self.assertRaises(NameNotDefined): + self.t("c", None) + + def test_name_handler_raises_error(self): + # What happens if our name-handler raises a different kind of error? + # we want it to ripple up all the way... + + def name_handler(_node): + return {}["test"] + + self.s.names = name_handler + + # This should never be accessed: + self.s.functions = {"c": 42} + + with self.assertRaises(KeyError): + self.t("c", None) + class TestWhitespace(DRYTest): """test that incorrect whitespace (preceding/trailing) doesn't matter."""