Skip to content

Commit 641fd91

Browse files
committed
Adding option to traverse breadth or depth first up ancestor hierarchy
1 parent ed42ba5 commit 641fd91

File tree

2 files changed

+51
-7
lines changed

2 files changed

+51
-7
lines changed

linkml_runtime/utils/schemaview.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
from functools import lru_cache
55
from copy import copy, deepcopy
6-
from collections import defaultdict
6+
from collections import defaultdict, OrderedDict
77
from typing import Mapping, Tuple, Type
88
from linkml_runtime.utils.namespaces import Namespaces
99
from deprecated.classic import deprecated
@@ -30,22 +30,28 @@
3030
ENUM_NAME = Union[EnumDefinitionName, str]
3131

3232

33-
def _closure(f, x, reflexive=True, **kwargs):
33+
def _closure(f, x, reflexive=True, depth_first=True, **kwargs):
3434
if reflexive:
3535
rv = [x]
3636
else:
3737
rv = []
3838
visited = []
3939
todo = [x]
4040
while len(todo) > 0:
41-
i = todo.pop()
41+
if depth_first:
42+
i = todo.pop()
43+
else:
44+
i = todo[0]
45+
todo = todo[1:]
4246
visited.append(i)
4347
vals = f(i)
4448
for v in vals:
4549
if v not in visited:
4650
todo.append(v)
47-
rv.append(v)
51+
if v not in rv:
52+
rv.append(v)
4853
return rv
54+
#return list(OrderedDict.fromkeys(rv))
4955

5056

5157
def load_schema_wrap(path: str, **kwargs):
@@ -498,7 +504,8 @@ def slot_children(self, slot_name: SLOT_NAME, imports=True, mixins=True, is_a=Tr
498504
return [x.name for x in elts if (x.is_a == slot_name and is_a) or (mixins and slot_name in x.mixins)]
499505

500506
@lru_cache()
501-
def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ClassDefinitionName]:
507+
def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True,
508+
depth_first=True) -> List[ClassDefinitionName]:
502509
"""
503510
Closure of class_parents method
504511
@@ -507,9 +514,12 @@ def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, ref
507514
:param mixins: include mixins (default is True)
508515
:param is_a: include is_a parents (default is True)
509516
:param reflexive: include self in set of ancestors
517+
:param depth_first:
510518
:return: ancestor class names
511519
"""
512-
return _closure(lambda x: self.class_parents(x, imports=imports, mixins=mixins, is_a=is_a), class_name, reflexive=reflexive)
520+
return _closure(lambda x: self.class_parents(x, imports=imports, mixins=mixins, is_a=is_a),
521+
class_name,
522+
reflexive=reflexive, depth_first=depth_first)
513523

514524
@lru_cache()
515525
def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[SlotDefinitionName]:
@@ -523,7 +533,9 @@ def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflex
523533
:param reflexive: include self in set of ancestors
524534
:return: ancestor slot names
525535
"""
526-
return _closure(lambda x: self.slot_parents(x, imports=imports, mixins=mixins, is_a=is_a), slot_name, reflexive=reflexive)
536+
return _closure(lambda x: self.slot_parents(x, imports=imports, mixins=mixins, is_a=is_a),
537+
slot_name,
538+
reflexive=reflexive)
527539

528540
@lru_cache()
529541
def class_descendants(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ClassDefinitionName]:

tests/test_utils/test_schemaview.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import unittest
33
import logging
44
from copy import copy
5+
from typing import List
56

67
from linkml_runtime.linkml_model.meta import SchemaDefinition, ClassDefinition, SlotDefinitionName, SlotDefinition
78
from linkml_runtime.loaders.yaml_loader import YAMLLoader
@@ -290,6 +291,37 @@ def test_merge_imports(self):
290291
all_c2_noi = copy(view.all_classes(imports=False))
291292
assert len(all_c2_noi) == len(all_c2)
292293

294+
def test_traversal(self):
295+
schema = SchemaDefinition(id='test', name='traversal-test')
296+
view = SchemaView(schema)
297+
view.add_class(ClassDefinition('Root', mixins=['RootMixin']))
298+
view.add_class(ClassDefinition('A', is_a='Root', mixins=['Am1', 'Am2', 'AZ']))
299+
view.add_class(ClassDefinition('B', is_a='A', mixins=['Bm1', 'Bm2', 'BY']))
300+
view.add_class(ClassDefinition('C', is_a='B', mixins=['Cm1', 'Cm2', 'CX']))
301+
view.add_class(ClassDefinition('RootMixin', mixin=True))
302+
view.add_class(ClassDefinition('Am1', is_a='RootMixin', mixin=True))
303+
view.add_class(ClassDefinition('Am2', is_a='RootMixin', mixin=True))
304+
view.add_class(ClassDefinition('Bm1', is_a='Am1', mixin=True))
305+
view.add_class(ClassDefinition('Bm2', is_a='Am2', mixin=True))
306+
view.add_class(ClassDefinition('Cm1', is_a='Bm1', mixin=True))
307+
view.add_class(ClassDefinition('Cm2', is_a='Bm2', mixin=True))
308+
view.add_class(ClassDefinition('AZ', is_a='RootMixin', mixin=True))
309+
view.add_class(ClassDefinition('BY', is_a='RootMixin', mixin=True))
310+
view.add_class(ClassDefinition('CX', is_a='RootMixin', mixin=True))
311+
def check(ancs: List, expected: List):
312+
#print(ancs)
313+
self.assertEqual(ancs, expected)
314+
check(view.class_ancestors('C', depth_first=True),
315+
['C', 'Cm1', 'Cm2', 'CX', 'B', 'Bm1', 'Bm2', 'BY', 'A', 'Am1', 'Am2', 'AZ', 'Root', 'RootMixin'])
316+
check(view.class_ancestors('C', depth_first=False),
317+
['C', 'Cm1', 'Cm2', 'CX', 'B', 'Bm1', 'Bm2', 'RootMixin', 'BY', 'A', 'Am1', 'Am2', 'AZ', 'Root'])
318+
check(view.class_ancestors('C', mixins=False),
319+
['C', 'B', 'A', 'Root'])
320+
check(view.class_ancestors('C', is_a=False),
321+
['C', 'Cm1', 'Cm2', 'CX'])
322+
323+
324+
293325
def test_slot_inheritance(self):
294326
schema = SchemaDefinition(id='test', name='test')
295327
view = SchemaView(schema)

0 commit comments

Comments
 (0)