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

fixed eval expression doctests and switched to pytest #315

Merged
merged 1 commit into from
Mar 22, 2024
Merged
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
15 changes: 8 additions & 7 deletions linkml_runtime/utils/eval_utils.py
Original file line number Diff line number Diff line change
@@ -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:
"""
@@ -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:

@@ -92,9 +96,6 @@ def eval_expr(expr: str, **kwargs) -> Any:






def eval_(node, bindings={}):
if isinstance(node, ast.Num):
return node.n
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


@@ -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])")