Skip to content

Commit

Permalink
Merge pull request #315 from linkml/eval_utils_docs
Browse files Browse the repository at this point in the history
fixed eval expression doctests and switched to pytest
  • Loading branch information
cmungall authored Mar 22, 2024
2 parents 27990c1 + ed36311 commit 146d305
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 91 deletions.
15 changes: 8 additions & 7 deletions linkml_runtime/utils/eval_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@

def eval_conditional(*conds: List[Tuple[bool, Any]]) -> Any:
"""
>>> cond(x < 25 : 'low', x > 25 : 'high', True: 'low')
Evaluate a collection of expression,value tuples, returing the first value whose expression is true
>>> x= 40
>>> eval_conditional((x < 25, 'low'), (x > 25, 'high'), (True, 'low'))
'high'
:param subj:
:return:
"""
Expand Down Expand Up @@ -58,10 +63,9 @@ def eval_expr(expr: str, **kwargs) -> Any:
Nulls:
- If a variable is enclosed in {}s then entire expression will eval to None if variable is unset
- If a variable is enclosed in {}s then entire expression will eval to None if any variable is unset
>>> eval_expr('{x} + {y}', x=None, y=2)
None
>>> assert eval_expr('{x} + {y}', x=None, y=2) is None
Functions:
Expand Down Expand Up @@ -92,9 +96,6 @@ def eval_expr(expr: str, **kwargs) -> Any:






def eval_(node, bindings={}):
if isinstance(node, ast.Num):
return node.n
Expand Down
137 changes: 53 additions & 84 deletions tests/test_utils/test_eval_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import unittest
from dataclasses import dataclass
from typing import List, Dict

import pytest

from linkml_runtime.utils.eval_utils import eval_expr


Expand All @@ -24,90 +25,58 @@ class Container:
person_index: Dict[str, Person] = None


class EvalUtilsTestCase(unittest.TestCase):
"""
Tests for linkml_runtime.utils.eval_utils
"""

def test_eval_expressions(self):
"""
Tests evaluation of expressions using eval_expr
"""
x = eval_expr("1 + 2")
self.assertEqual(x, 3)
self.assertEqual(eval_expr("1 + 2 + 3"), 6)
x = eval_expr("{z} + 2", z=1)
self.assertEqual(x, 3)
self.assertIsNone(eval_expr('{x} + {y}', x=5, y=None))
x = eval_expr("'x' + 'y'")
assert x == 'xy'
#x = eval_expr("'{x}' + '{y}'", x='a', y='b')
#self.assertEqual(x, 'ab')
self.assertEqual(eval_expr("['a','b'] + ['c','d']"), ['a', 'b', 'c', 'd'])
self.assertEqual(eval_expr("{x} + {y}", x=['a', 'b'], y=['c', 'd']), ['a', 'b', 'c', 'd'])
self.assertEqual(eval_expr("{'a': 1}"), {'a': 1})
self.assertEqual(eval_expr("max([1, 5, 2])"), 5)
self.assertEqual(eval_expr("max({x})", x=[1, 5, 2]), 5)
self.assertEqual(eval_expr("True"), True)
self.assertEqual(eval_expr("False"), False)
self.assertEqual(eval_expr("1 + 1 == 3"), False)
self.assertEqual(eval_expr("1 < 2"), True)
self.assertEqual(eval_expr("1 <= 1"), True)
self.assertEqual(eval_expr("1 >= 1"), True)
self.assertEqual(eval_expr("2 > 1"), True)
self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=1), 'EQ')
self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2), 'NEQ')
self.assertEqual(eval_expr("'NOT_NULL' if x else 'NULL'", x=1), 'NOT_NULL')
self.assertEqual(eval_expr("'NOT_NULL' if x else 'NULL'", x=None), 'NULL')
self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2), 'NEQ')
case = "case(({x} < 25, 'LOW'), ({x} > 75, 'HIGH'), (True, 'MEDIUM'))"
self.assertEqual(eval_expr(case, x=10), 'LOW')
self.assertEqual(eval_expr(case, x=100), 'HIGH')
self.assertEqual(eval_expr(case, x=50), 'MEDIUM')
self.assertEqual(eval_expr('x', x='a'), 'a')
self.assertEqual(eval_expr('x+y', x=1, y=2), 3)
# todo
self.assertEqual(eval_expr('x["a"] + y', x={'a': 1}, y=2), 3)
self.assertEqual(eval_expr('x["a"]["b"] + y', x={'a': {'b': 1}}, y=2), 3)
p = Person(name='x', aliases=['a', 'b', 'c'], address=Address(street='1 x street'))
self.assertEqual(eval_expr('p.name', p=p), 'x')
self.assertEqual(eval_expr('p.address.street', p=p), '1 x street')
self.assertEqual(eval_expr('len(p.aliases)', p=p), 3)
self.assertEqual(eval_expr('p.aliases', p=p), p.aliases)
p2 = Person(name='x2', aliases=['a2', 'b2', 'c2'], address=Address(street='2 x street'))
c = Container(persons=[p, p2])
x = eval_expr('c.persons.name', c=c)
self.assertEqual(x, ['x', 'x2'])
x = eval_expr('c.persons.address.street', c=c)
self.assertEqual(x, ['1 x street', '2 x street'])
x = eval_expr('strlen(c.persons.address.street)', c=c)
self.assertEqual(x, [10, 10])
c = Container(person_index={p.name: p, p2.name: p2})
x = eval_expr('c.person_index.name', c=c)
#print(x)
self.assertEqual(x, ['x', 'x2'])
x = eval_expr('c.person_index.address.street', c=c)
self.assertEqual(x, ['1 x street', '2 x street'])
x = eval_expr('strlen(c.person_index.name)', c=c)
self.assertEqual(x, [1, 2])
#self.assertEqual('x', eval_expr('"x" if True else "y"'))

def test_no_eval_prohibited(self):
"""
Ensure that certain patterns cannot be evaluated
def test_eval_expressions():
assert eval_expr("1 + 2") == 3
assert eval_expr("1 + 2 + 3") == 6
assert eval_expr("{z} + 2", z=1) == 3
assert eval_expr('{x} + {y}', x=5, y=None) is None
assert eval_expr("'x' + 'y'") == 'xy'
assert eval_expr("['a','b'] + ['c','d']") == ['a', 'b', 'c', 'd']
assert eval_expr("{x} + {y}", x=['a', 'b'], y=['c', 'd']) == ['a', 'b', 'c', 'd']
assert eval_expr("{'a': 1}") == {'a': 1}
assert eval_expr("max([1, 5, 2])") == 5
assert eval_expr("max({x})", x=[1, 5, 2]) == 5
assert eval_expr("True") is True
assert eval_expr("False") is False
assert eval_expr("1 + 1 == 3") is False
assert eval_expr("1 < 2") is True
assert eval_expr("1 <= 1") is True
assert eval_expr("1 >= 1") is True
assert eval_expr("2 > 1") is True
assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=1) == 'EQ'
assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2) == 'NEQ'
assert eval_expr("'NOT_NULL' if x else 'NULL'", x=1) == 'NOT_NULL'
assert eval_expr("'NOT_NULL' if x else 'NULL'", x=None) == 'NULL'
assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2) == 'NEQ'
case = "case(({x} < 25, 'LOW'), ({x} > 75, 'HIGH'), (True, 'MEDIUM'))"
assert eval_expr(case, x=10) == 'LOW'
assert eval_expr(case, x=100) == 'HIGH'
assert eval_expr(case, x=50) == 'MEDIUM'
assert eval_expr('x', x='a') == 'a'
assert eval_expr('x+y', x=1, y=2) == 3
assert eval_expr('x["a"] + y', x={'a': 1}, y=2) == 3
assert eval_expr('x["a"]["b"] + y', x={'a': {'b': 1}}, y=2) == 3
p = Person(name='x', aliases=['a', 'b', 'c'], address=Address(street='1 x street'))
assert eval_expr('p.name', p=p) == 'x'
assert eval_expr('p.address.street', p=p) == '1 x street'
assert eval_expr('len(p.aliases)', p=p) == 3
assert eval_expr('p.aliases', p=p) == p.aliases
p2 = Person(name='x2', aliases=['a2', 'b2', 'c2'], address=Address(street='2 x street'))
c = Container(persons=[p, p2])
assert eval_expr('c.persons.name', c=c) == ['x', 'x2']
assert eval_expr('c.persons.address.street', c=c) == ['1 x street', '2 x street']
assert eval_expr('strlen(c.persons.address.street)', c=c) == [10, 10]
c = Container(person_index={p.name: p, p2.name: p2})
assert eval_expr('c.person_index.name', c=c) == ['x', 'x2']
assert eval_expr('c.person_index.address.street', c=c) == ['1 x street', '2 x street']
assert eval_expr('strlen(c.person_index.name)', c=c) == [1, 2]

See `<https://stackoverflow.com/questions/2371436/evaluating-a-mathematical-expression-in-a-string>`_
"""
with self.assertRaises(NotImplementedError):
eval_expr("__import__('os').listdir()")

def test_funcs(self):
"""
Not yet implemented
"""
with self.assertRaises(NotImplementedError):
eval_expr("my_func([1,2,3])")
def test_no_eval_prohibited():
with pytest.raises(NotImplementedError):
eval_expr("__import__('os').listdir()")


if __name__ == '__main__':
unittest.main()
def test_funcs():
with pytest.raises(NotImplementedError):
eval_expr("my_func([1,2,3])")

0 comments on commit 146d305

Please sign in to comment.