Skip to content

Commit 45f52cd

Browse files
Merge branch 'main' into pretty-print
2 parents 3298d67 + 146d305 commit 45f52cd

File tree

4 files changed

+124
-111
lines changed

4 files changed

+124
-111
lines changed

linkml_runtime/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class OrderingError(RuntimeError):
2+
"""Exception raised when there is a problem with SchemaView ordering"""

linkml_runtime/utils/eval_utils.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717

1818
def eval_conditional(*conds: List[Tuple[bool, Any]]) -> Any:
1919
"""
20-
>>> cond(x < 25 : 'low', x > 25 : 'high', True: 'low')
20+
Evaluate a collection of expression,value tuples, returing the first value whose expression is true
21+
22+
>>> x= 40
23+
>>> eval_conditional((x < 25, 'low'), (x > 25, 'high'), (True, 'low'))
24+
'high'
25+
2126
:param subj:
2227
:return:
2328
"""
@@ -58,10 +63,9 @@ def eval_expr(expr: str, **kwargs) -> Any:
5863
5964
Nulls:
6065
61-
- If a variable is enclosed in {}s then entire expression will eval to None if variable is unset
66+
- If a variable is enclosed in {}s then entire expression will eval to None if any variable is unset
6267
63-
>>> eval_expr('{x} + {y}', x=None, y=2)
64-
None
68+
>>> assert eval_expr('{x} + {y}', x=None, y=2) is None
6569
6670
Functions:
6771
@@ -92,9 +96,6 @@ def eval_expr(expr: str, **kwargs) -> Any:
9296

9397

9498

95-
96-
97-
9899
def eval_(node, bindings={}):
99100
if isinstance(node, ast.Num):
100101
return node.n

linkml_runtime/utils/schemaview.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
from copy import copy, deepcopy
77
from collections import defaultdict, deque
88
from pathlib import Path
9-
from typing import Mapping, Tuple
9+
from typing import Mapping, Tuple, TypeVar
1010
import warnings
1111

1212
from linkml_runtime.utils.namespaces import Namespaces
1313
from deprecated.classic import deprecated
1414
from linkml_runtime.utils.context_utils import parse_import_map, map_import
1515
from linkml_runtime.utils.pattern import PatternResolver
1616
from linkml_runtime.linkml_model.meta import *
17+
from linkml_runtime.exceptions import OrderingError
1718
from enum import Enum
1819

1920
logger = logging.getLogger(__name__)
@@ -33,11 +34,23 @@
3334
TYPE_NAME = Union[TypeDefinitionName, str]
3435
ENUM_NAME = Union[EnumDefinitionName, str]
3536

37+
ElementType = TypeVar("ElementType", bound=Element)
38+
ElementNameType = TypeVar("ElementNameType", bound=Union[ElementName,str])
39+
DefinitionType = TypeVar("DefinitionType", bound=Definition)
40+
DefinitionNameType = TypeVar("DefinitionNameType", bound=Union[DefinitionName,str])
41+
ElementDict = Dict[ElementNameType, ElementType]
42+
DefDict = Dict[DefinitionNameType, DefinitionType]
43+
3644

3745
class OrderedBy(Enum):
3846
RANK = "rank"
3947
LEXICAL = "lexical"
4048
PRESERVE = "preserve"
49+
INHERITANCE = "inheritance"
50+
"""
51+
Order according to inheritance such that if C is a child of P then C appears after P
52+
"""
53+
4154

4255

4356
def _closure(f, x, reflexive=True, depth_first=True, **kwargs):
@@ -305,7 +318,22 @@ def all_class(self, imports=True) -> Dict[ClassDefinitionName, ClassDefinition]:
305318
"""
306319
return self._get_dict(CLASSES, imports)
307320

308-
def _order_lexically(self, elements: dict):
321+
def ordered(self, elements: ElementDict, ordered_by: Optional[OrderedBy] = None) -> ElementDict:
322+
"""
323+
Order a dictionary of elements with some ordering method in :class:`.OrderedBy`
324+
"""
325+
if ordered_by in (OrderedBy.LEXICAL, OrderedBy.LEXICAL.value):
326+
return self._order_lexically(elements)
327+
elif ordered_by in (OrderedBy.RANK, OrderedBy.RANK.value):
328+
return self._order_rank(elements)
329+
elif ordered_by in (OrderedBy.INHERITANCE, OrderedBy.INHERITANCE.value):
330+
return self._order_inheritance(elements)
331+
elif ordered_by is None or ordered_by in (OrderedBy.PRESERVE, OrderedBy.PRESERVE.value):
332+
return elements
333+
else:
334+
raise ValueError(f"ordered_by must be in OrderedBy or None, got {ordered_by}")
335+
336+
def _order_lexically(self, elements: ElementDict) -> ElementDict:
309337
"""
310338
:param element: slots or class type to order
311339
:param imports
@@ -320,7 +348,7 @@ def _order_lexically(self, elements: dict):
320348
ordered_elements[self.get_element(name).name] = self.get_element(name)
321349
return ordered_elements
322350

323-
def _order_rank(self, elements: dict):
351+
def _order_rank(self, elements: ElementDict) -> ElementDict:
324352
"""
325353
:param elements: slots or classes to order
326354
:return: all classes or slots sorted by their rank in schema view
@@ -342,6 +370,32 @@ def _order_rank(self, elements: dict):
342370
rank_ordered_elements.update(unranked_map)
343371
return rank_ordered_elements
344372

373+
def _order_inheritance(self, elements: DefDict) -> DefDict:
374+
"""
375+
sort classes such that if C is a child of P then C appears after P in the list
376+
"""
377+
clist = list(elements.values())
378+
slist = [] # sorted
379+
can_add = False
380+
while len(clist) > 0:
381+
for i in range(len(clist)):
382+
candidate = clist[i]
383+
can_add = False
384+
if candidate.is_a is None:
385+
can_add = True
386+
else:
387+
if candidate.is_a in [p.name for p in slist]:
388+
can_add = True
389+
if can_add:
390+
slist = slist + [candidate]
391+
del clist[i]
392+
break
393+
if not can_add:
394+
raise OrderingError(f"could not find suitable element in {clist} that does not ref {slist}")
395+
396+
return {s.name: s for s in slist}
397+
398+
345399
@lru_cache(None)
346400
def all_classes(self, ordered_by=OrderedBy.PRESERVE, imports=True) -> Dict[ClassDefinitionName, ClassDefinition]:
347401
"""
@@ -350,15 +404,8 @@ def all_classes(self, ordered_by=OrderedBy.PRESERVE, imports=True) -> Dict[Class
350404
:return: all classes in schema view
351405
"""
352406
classes = copy(self._get_dict(CLASSES, imports))
353-
354-
if ordered_by == OrderedBy.LEXICAL:
355-
ordered_classes = self._order_lexically(elements=classes)
356-
elif ordered_by == OrderedBy.RANK:
357-
ordered_classes = self._order_rank(elements=classes)
358-
else: # else preserve the order in the yaml
359-
ordered_classes = classes
360-
361-
return ordered_classes
407+
classes = self.ordered(classes, ordered_by=ordered_by)
408+
return classes
362409

363410
@deprecated("Use `all_slots` instead")
364411
@lru_cache(None)
@@ -386,14 +433,8 @@ def all_slots(self, ordered_by=OrderedBy.PRESERVE, imports=True, attributes=True
386433
if aname not in slots:
387434
slots[aname] = a
388435

389-
if ordered_by == OrderedBy.LEXICAL:
390-
ordered_slots = self._order_lexically(elements=slots)
391-
elif ordered_by == OrderedBy.RANK:
392-
ordered_slots = self._order_rank(elements=slots)
393-
else:
394-
# preserve order in YAML
395-
ordered_slots = slots
396-
return ordered_slots
436+
slots = self.ordered(slots, ordered_by=ordered_by)
437+
return slots
397438

398439
@deprecated("Use `all_enums` instead")
399440
@lru_cache(None)

tests/test_utils/test_eval_utils.py

Lines changed: 53 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import unittest
21
from dataclasses import dataclass
32
from typing import List, Dict
43

4+
import pytest
5+
56
from linkml_runtime.utils.eval_utils import eval_expr
67

78

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

2627

27-
class EvalUtilsTestCase(unittest.TestCase):
28-
"""
29-
Tests for linkml_runtime.utils.eval_utils
30-
"""
31-
32-
def test_eval_expressions(self):
33-
"""
34-
Tests evaluation of expressions using eval_expr
35-
"""
36-
x = eval_expr("1 + 2")
37-
self.assertEqual(x, 3)
38-
self.assertEqual(eval_expr("1 + 2 + 3"), 6)
39-
x = eval_expr("{z} + 2", z=1)
40-
self.assertEqual(x, 3)
41-
self.assertIsNone(eval_expr('{x} + {y}', x=5, y=None))
42-
x = eval_expr("'x' + 'y'")
43-
assert x == 'xy'
44-
#x = eval_expr("'{x}' + '{y}'", x='a', y='b')
45-
#self.assertEqual(x, 'ab')
46-
self.assertEqual(eval_expr("['a','b'] + ['c','d']"), ['a', 'b', 'c', 'd'])
47-
self.assertEqual(eval_expr("{x} + {y}", x=['a', 'b'], y=['c', 'd']), ['a', 'b', 'c', 'd'])
48-
self.assertEqual(eval_expr("{'a': 1}"), {'a': 1})
49-
self.assertEqual(eval_expr("max([1, 5, 2])"), 5)
50-
self.assertEqual(eval_expr("max({x})", x=[1, 5, 2]), 5)
51-
self.assertEqual(eval_expr("True"), True)
52-
self.assertEqual(eval_expr("False"), False)
53-
self.assertEqual(eval_expr("1 + 1 == 3"), False)
54-
self.assertEqual(eval_expr("1 < 2"), True)
55-
self.assertEqual(eval_expr("1 <= 1"), True)
56-
self.assertEqual(eval_expr("1 >= 1"), True)
57-
self.assertEqual(eval_expr("2 > 1"), True)
58-
self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=1), 'EQ')
59-
self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2), 'NEQ')
60-
self.assertEqual(eval_expr("'NOT_NULL' if x else 'NULL'", x=1), 'NOT_NULL')
61-
self.assertEqual(eval_expr("'NOT_NULL' if x else 'NULL'", x=None), 'NULL')
62-
self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2), 'NEQ')
63-
case = "case(({x} < 25, 'LOW'), ({x} > 75, 'HIGH'), (True, 'MEDIUM'))"
64-
self.assertEqual(eval_expr(case, x=10), 'LOW')
65-
self.assertEqual(eval_expr(case, x=100), 'HIGH')
66-
self.assertEqual(eval_expr(case, x=50), 'MEDIUM')
67-
self.assertEqual(eval_expr('x', x='a'), 'a')
68-
self.assertEqual(eval_expr('x+y', x=1, y=2), 3)
69-
# todo
70-
self.assertEqual(eval_expr('x["a"] + y', x={'a': 1}, y=2), 3)
71-
self.assertEqual(eval_expr('x["a"]["b"] + y', x={'a': {'b': 1}}, y=2), 3)
72-
p = Person(name='x', aliases=['a', 'b', 'c'], address=Address(street='1 x street'))
73-
self.assertEqual(eval_expr('p.name', p=p), 'x')
74-
self.assertEqual(eval_expr('p.address.street', p=p), '1 x street')
75-
self.assertEqual(eval_expr('len(p.aliases)', p=p), 3)
76-
self.assertEqual(eval_expr('p.aliases', p=p), p.aliases)
77-
p2 = Person(name='x2', aliases=['a2', 'b2', 'c2'], address=Address(street='2 x street'))
78-
c = Container(persons=[p, p2])
79-
x = eval_expr('c.persons.name', c=c)
80-
self.assertEqual(x, ['x', 'x2'])
81-
x = eval_expr('c.persons.address.street', c=c)
82-
self.assertEqual(x, ['1 x street', '2 x street'])
83-
x = eval_expr('strlen(c.persons.address.street)', c=c)
84-
self.assertEqual(x, [10, 10])
85-
c = Container(person_index={p.name: p, p2.name: p2})
86-
x = eval_expr('c.person_index.name', c=c)
87-
#print(x)
88-
self.assertEqual(x, ['x', 'x2'])
89-
x = eval_expr('c.person_index.address.street', c=c)
90-
self.assertEqual(x, ['1 x street', '2 x street'])
91-
x = eval_expr('strlen(c.person_index.name)', c=c)
92-
self.assertEqual(x, [1, 2])
93-
#self.assertEqual('x', eval_expr('"x" if True else "y"'))
94-
95-
def test_no_eval_prohibited(self):
96-
"""
97-
Ensure that certain patterns cannot be evaluated
28+
def test_eval_expressions():
29+
assert eval_expr("1 + 2") == 3
30+
assert eval_expr("1 + 2 + 3") == 6
31+
assert eval_expr("{z} + 2", z=1) == 3
32+
assert eval_expr('{x} + {y}', x=5, y=None) is None
33+
assert eval_expr("'x' + 'y'") == 'xy'
34+
assert eval_expr("['a','b'] + ['c','d']") == ['a', 'b', 'c', 'd']
35+
assert eval_expr("{x} + {y}", x=['a', 'b'], y=['c', 'd']) == ['a', 'b', 'c', 'd']
36+
assert eval_expr("{'a': 1}") == {'a': 1}
37+
assert eval_expr("max([1, 5, 2])") == 5
38+
assert eval_expr("max({x})", x=[1, 5, 2]) == 5
39+
assert eval_expr("True") is True
40+
assert eval_expr("False") is False
41+
assert eval_expr("1 + 1 == 3") is False
42+
assert eval_expr("1 < 2") is True
43+
assert eval_expr("1 <= 1") is True
44+
assert eval_expr("1 >= 1") is True
45+
assert eval_expr("2 > 1") is True
46+
assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=1) == 'EQ'
47+
assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2) == 'NEQ'
48+
assert eval_expr("'NOT_NULL' if x else 'NULL'", x=1) == 'NOT_NULL'
49+
assert eval_expr("'NOT_NULL' if x else 'NULL'", x=None) == 'NULL'
50+
assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2) == 'NEQ'
51+
case = "case(({x} < 25, 'LOW'), ({x} > 75, 'HIGH'), (True, 'MEDIUM'))"
52+
assert eval_expr(case, x=10) == 'LOW'
53+
assert eval_expr(case, x=100) == 'HIGH'
54+
assert eval_expr(case, x=50) == 'MEDIUM'
55+
assert eval_expr('x', x='a') == 'a'
56+
assert eval_expr('x+y', x=1, y=2) == 3
57+
assert eval_expr('x["a"] + y', x={'a': 1}, y=2) == 3
58+
assert eval_expr('x["a"]["b"] + y', x={'a': {'b': 1}}, y=2) == 3
59+
p = Person(name='x', aliases=['a', 'b', 'c'], address=Address(street='1 x street'))
60+
assert eval_expr('p.name', p=p) == 'x'
61+
assert eval_expr('p.address.street', p=p) == '1 x street'
62+
assert eval_expr('len(p.aliases)', p=p) == 3
63+
assert eval_expr('p.aliases', p=p) == p.aliases
64+
p2 = Person(name='x2', aliases=['a2', 'b2', 'c2'], address=Address(street='2 x street'))
65+
c = Container(persons=[p, p2])
66+
assert eval_expr('c.persons.name', c=c) == ['x', 'x2']
67+
assert eval_expr('c.persons.address.street', c=c) == ['1 x street', '2 x street']
68+
assert eval_expr('strlen(c.persons.address.street)', c=c) == [10, 10]
69+
c = Container(person_index={p.name: p, p2.name: p2})
70+
assert eval_expr('c.person_index.name', c=c) == ['x', 'x2']
71+
assert eval_expr('c.person_index.address.street', c=c) == ['1 x street', '2 x street']
72+
assert eval_expr('strlen(c.person_index.name)', c=c) == [1, 2]
9873

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

104-
def test_funcs(self):
105-
"""
106-
Not yet implemented
107-
"""
108-
with self.assertRaises(NotImplementedError):
109-
eval_expr("my_func([1,2,3])")
75+
def test_no_eval_prohibited():
76+
with pytest.raises(NotImplementedError):
77+
eval_expr("__import__('os').listdir()")
11078

11179

112-
if __name__ == '__main__':
113-
unittest.main()
80+
def test_funcs():
81+
with pytest.raises(NotImplementedError):
82+
eval_expr("my_func([1,2,3])")

0 commit comments

Comments
 (0)