Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't misuse KeyError for the custom names function. #126

Merged
merged 2 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ 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
.. code-block:: pycon

>>> import ast
>>> from simpleeval import safe_power
Expand Down Expand Up @@ -200,15 +200,15 @@ If Expressions

You can use python style ``if x then y else z`` type expressions:

.. code-block:: python
.. code-block:: pycon

>>> simple_eval("'equal' if x == y else 'not equal'",
names={"x": 1, "y": 2})
'not equal'

which, of course, can be nested:

.. code-block:: python
.. code-block:: pycon

>>> simple_eval("'a' if 1 == 2 else 'b' if 2 == 3 else 'c'")
'c'
Expand All @@ -219,15 +219,15 @@ Functions

You can define functions which you'd like the expresssions to have access to:

.. code-block:: python
.. code-block:: pycon

>>> simple_eval("double(21)", functions={"double": lambda x:x*2})
42

You can define "real" functions to pass in rather than lambdas, of course too,
and even re-name them so that expressions can be shorter

.. code-block:: python
.. code-block:: pycon

>>> def double(x):
return x * 2
Expand All @@ -252,7 +252,7 @@ are provided in the ``DEFAULT_FUNCTIONS`` dict:
If you want to provide a list of functions, but want to keep these as well,
then you can do a normal python ``.copy()`` & ``.update``:

.. code-block:: python
.. code-block:: pycon

>>> my_functions = simpleeval.DEFAULT_FUNCTIONS.copy()
>>> my_functions.update(
Expand All @@ -267,15 +267,15 @@ Names
Sometimes it's useful to have variables available, which in python terminology
are called 'names'.

.. code-block:: python
.. code-block:: pycon

>>> simple_eval("a + b", names={"a": 11, "b": 100})
111

You can also hand the handling of names over to a function, if you prefer:


.. code-block:: python
.. code-block:: pycon

>>> def name_handler(node):
return ord(node.id[0].lower(a))-96
Expand All @@ -284,9 +284,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.

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 an "I can't find that name!", then it should raise a ``simpleeval.NameNotDefined``
exception. Eg:

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).
.. code-block:: pycon

>>> 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
---------------------------
Expand All @@ -296,7 +321,7 @@ evaluations, you can create a SimpleEval object, and pass it expressions each
time (which should be a bit quicker, and certainly more convenient for some use
cases):

.. code-block:: python
.. code-block:: pycon

>>> s = SimpleEval()

Expand Down Expand Up @@ -375,7 +400,7 @@ comprehensions.
Since the primary intention of this library is short expressions - an extra 'sweetener' is
enabled by default. You can access a dict (or similar's) keys using the .attr syntax:

.. code-block:: python
.. code-block:: pycon

>>> simple_eval("foo.bar", names={"foo": {"bar": 42}})
42
Expand Down
22 changes: 14 additions & 8 deletions simpleeval.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,24 +554,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
danthedeckie marked this conversation as resolved.
Show resolved Hide resolved
# (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)
Expand Down
30 changes: 30 additions & 0 deletions test_simpleeval.py
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,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."""
Expand Down