Skip to content

Commit 014f2e8

Browse files
authored
Merge pull request #126 from danthedeckie/better-names-exceptions
Don't misuse KeyError for the custom `names` function.
2 parents 166e90f + ee16fd3 commit 014f2e8

File tree

3 files changed

+81
-20
lines changed

3 files changed

+81
-20
lines changed

Diff for: README.rst

+37-12
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ The ``^`` operator is often mistaken for a exponent operator, not the bitwise
157157
operation that it is in python, so if you want ``3 ^ 2`` to equal ``9``, you can
158158
replace the operator like this:
159159

160-
.. code-block:: python
160+
.. code-block:: pycon
161161
162162
>>> import ast
163163
>>> from simpleeval import safe_power
@@ -200,15 +200,15 @@ If Expressions
200200

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

203-
.. code-block:: python
203+
.. code-block:: pycon
204204
205205
>>> simple_eval("'equal' if x == y else 'not equal'",
206206
names={"x": 1, "y": 2})
207207
'not equal'
208208
209209
which, of course, can be nested:
210210

211-
.. code-block:: python
211+
.. code-block:: pycon
212212
213213
>>> simple_eval("'a' if 1 == 2 else 'b' if 2 == 3 else 'c'")
214214
'c'
@@ -219,15 +219,15 @@ Functions
219219

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

222-
.. code-block:: python
222+
.. code-block:: pycon
223223
224224
>>> simple_eval("double(21)", functions={"double": lambda x:x*2})
225225
42
226226
227227
You can define "real" functions to pass in rather than lambdas, of course too,
228228
and even re-name them so that expressions can be shorter
229229

230-
.. code-block:: python
230+
.. code-block:: pycon
231231
232232
>>> def double(x):
233233
return x * 2
@@ -252,7 +252,7 @@ are provided in the ``DEFAULT_FUNCTIONS`` dict:
252252
If you want to provide a list of functions, but want to keep these as well,
253253
then you can do a normal python ``.copy()`` & ``.update``:
254254

255-
.. code-block:: python
255+
.. code-block:: pycon
256256
257257
>>> my_functions = simpleeval.DEFAULT_FUNCTIONS.copy()
258258
>>> my_functions.update(
@@ -267,15 +267,15 @@ Names
267267
Sometimes it's useful to have variables available, which in python terminology
268268
are called 'names'.
269269

270-
.. code-block:: python
270+
.. code-block:: pycon
271271
272272
>>> simple_eval("a + b", names={"a": 11, "b": 100})
273273
111
274274
275275
You can also hand the handling of names over to a function, if you prefer:
276276

277277

278-
.. code-block:: python
278+
.. code-block:: pycon
279279
280280
>>> def name_handler(node):
281281
return ord(node.id[0].lower(a))-96
@@ -284,9 +284,34 @@ You can also hand the handling of names over to a function, if you prefer:
284284
3
285285
286286
That was a bit of a silly example, but you could use this for pulling values
287-
from a database or file, say, or doing some kind of caching system.
287+
from a database or file, looking up spreadsheet cells, say, or doing some kind of caching system.
288+
289+
In general, when it attempts to find a variable by name, if it cannot find one,
290+
then it will look in the ``functions`` for a function of that name. If you want your name handler
291+
function to return an "I can't find that name!", then it should raise a ``simpleeval.NameNotDefined``
292+
exception. Eg:
288293

289-
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).
294+
.. code-block:: pycon
295+
296+
>>> def name_handler(node):
297+
... if node.id[0] == 'a':
298+
... return 21
299+
... raise NameNotDefined(node.id[0], "Not found")
300+
...
301+
... simple_eval('a + a', names=name_handler, functions={"b": 100})
302+
303+
42
304+
305+
>>> simple_eval('a + b', names=name_handler, functions={'b': 100})
306+
121
307+
308+
(Note: in that example, putting a number directly into the ``functions`` dict was done just to
309+
show the fall-back to functions. Normally only put actual callables in there.)
310+
311+
312+
The two default names that are provided are ``True`` and ``False``. So if you want to provide
313+
your own names, but want ``True`` and ``False`` to keep working, either provide them yourself,
314+
or ``.copy()`` and ``.update`` the ``DEFAULT_NAMES``. (See functions example above).
290315

291316
Creating an Evaluator Class
292317
---------------------------
@@ -296,7 +321,7 @@ evaluations, you can create a SimpleEval object, and pass it expressions each
296321
time (which should be a bit quicker, and certainly more convenient for some use
297322
cases):
298323

299-
.. code-block:: python
324+
.. code-block:: pycon
300325
301326
>>> s = SimpleEval()
302327
@@ -375,7 +400,7 @@ comprehensions.
375400
Since the primary intention of this library is short expressions - an extra 'sweetener' is
376401
enabled by default. You can access a dict (or similar's) keys using the .attr syntax:
377402

378-
.. code-block:: python
403+
.. code-block:: pycon
379404
380405
>>> simple_eval("foo.bar", names={"foo": {"bar": 42}})
381406
42

Diff for: simpleeval.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -554,24 +554,30 @@ def _eval_name(self, node):
554554
try:
555555
# This happens at least for slicing
556556
# This is a safe thing to do because it is impossible
557-
# that there is a true exression assigning to none
557+
# that there is a true expression assigning to none
558558
# (the compiler rejects it, so you can't even
559559
# pass that to ast.parse)
560-
if hasattr(self.names, "__getitem__"):
561-
return self.names[node.id]
562-
if callable(self.names):
560+
return self.names[node.id]
561+
562+
except (TypeError, KeyError):
563+
pass
564+
565+
if callable(self.names):
566+
try:
563567
return self.names(node)
568+
except NameNotDefined:
569+
pass
570+
elif not hasattr(self.names, "__getitem__"):
564571
raise InvalidExpression(
565572
'Trying to use name (variable) "{0}"'
566573
' when no "names" defined for'
567574
" evaluator".format(node.id)
568575
)
569576

570-
except KeyError:
571-
if node.id in self.functions:
572-
return self.functions[node.id]
577+
if node.id in self.functions:
578+
return self.functions[node.id]
573579

574-
raise NameNotDefined(node.id, self.expr)
580+
raise NameNotDefined(node.id, self.expr)
575581

576582
def _eval_subscript(self, node):
577583
container = self._eval(node.value)

Diff for: test_simpleeval.py

+30
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,36 @@ def name_handler(node):
987987
self.t("a", 1)
988988
self.t("a + b", 3)
989989

990+
def test_name_handler_name_not_found(self):
991+
def name_handler(node):
992+
if node.id[0] == "a":
993+
return 21
994+
raise NameNotDefined(node.id[0], "not found")
995+
996+
self.s.names = name_handler
997+
self.s.functions = {"b": lambda: 100}
998+
self.t("a + a", 42)
999+
1000+
self.t("b()", 100)
1001+
1002+
with self.assertRaises(NameNotDefined):
1003+
self.t("c", None)
1004+
1005+
def test_name_handler_raises_error(self):
1006+
# What happens if our name-handler raises a different kind of error?
1007+
# we want it to ripple up all the way...
1008+
1009+
def name_handler(_node):
1010+
return {}["test"]
1011+
1012+
self.s.names = name_handler
1013+
1014+
# This should never be accessed:
1015+
self.s.functions = {"c": 42}
1016+
1017+
with self.assertRaises(KeyError):
1018+
self.t("c", None)
1019+
9901020

9911021
class TestWhitespace(DRYTest):
9921022
"""test that incorrect whitespace (preceding/trailing) doesn't matter."""

0 commit comments

Comments
 (0)