Skip to content

Commit

Permalink
Merge pull request #158 from linkml/schemaview-traversal-order
Browse files Browse the repository at this point in the history
Adding option to traverse breadth or depth first up ancestor hierarchy
  • Loading branch information
cmungall authored Mar 24, 2022
2 parents c0e7789 + 641fd91 commit b27337b
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 7 deletions.
26 changes: 19 additions & 7 deletions linkml_runtime/utils/schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from functools import lru_cache
from copy import copy, deepcopy
from collections import defaultdict
from collections import defaultdict, OrderedDict
from typing import Mapping, Tuple, Type
from linkml_runtime.utils.namespaces import Namespaces
from deprecated.classic import deprecated
Expand All @@ -30,22 +30,28 @@
ENUM_NAME = Union[EnumDefinitionName, str]


def _closure(f, x, reflexive=True, **kwargs):
def _closure(f, x, reflexive=True, depth_first=True, **kwargs):
if reflexive:
rv = [x]
else:
rv = []
visited = []
todo = [x]
while len(todo) > 0:
i = todo.pop()
if depth_first:
i = todo.pop()
else:
i = todo[0]
todo = todo[1:]
visited.append(i)
vals = f(i)
for v in vals:
if v not in visited:
todo.append(v)
rv.append(v)
if v not in rv:
rv.append(v)
return rv
#return list(OrderedDict.fromkeys(rv))


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

@lru_cache()
def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ClassDefinitionName]:
def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True,
depth_first=True) -> List[ClassDefinitionName]:
"""
Closure of class_parents method
Expand All @@ -507,9 +514,12 @@ def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, ref
:param mixins: include mixins (default is True)
:param is_a: include is_a parents (default is True)
:param reflexive: include self in set of ancestors
:param depth_first:
:return: ancestor class names
"""
return _closure(lambda x: self.class_parents(x, imports=imports, mixins=mixins, is_a=is_a), class_name, reflexive=reflexive)
return _closure(lambda x: self.class_parents(x, imports=imports, mixins=mixins, is_a=is_a),
class_name,
reflexive=reflexive, depth_first=depth_first)

@lru_cache()
def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[SlotDefinitionName]:
Expand All @@ -523,7 +533,9 @@ def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflex
:param reflexive: include self in set of ancestors
:return: ancestor slot names
"""
return _closure(lambda x: self.slot_parents(x, imports=imports, mixins=mixins, is_a=is_a), slot_name, reflexive=reflexive)
return _closure(lambda x: self.slot_parents(x, imports=imports, mixins=mixins, is_a=is_a),
slot_name,
reflexive=reflexive)

@lru_cache()
def class_descendants(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ClassDefinitionName]:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_utils/test_schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import unittest
import logging
from copy import copy
from typing import List

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

def test_traversal(self):
schema = SchemaDefinition(id='test', name='traversal-test')
view = SchemaView(schema)
view.add_class(ClassDefinition('Root', mixins=['RootMixin']))
view.add_class(ClassDefinition('A', is_a='Root', mixins=['Am1', 'Am2', 'AZ']))
view.add_class(ClassDefinition('B', is_a='A', mixins=['Bm1', 'Bm2', 'BY']))
view.add_class(ClassDefinition('C', is_a='B', mixins=['Cm1', 'Cm2', 'CX']))
view.add_class(ClassDefinition('RootMixin', mixin=True))
view.add_class(ClassDefinition('Am1', is_a='RootMixin', mixin=True))
view.add_class(ClassDefinition('Am2', is_a='RootMixin', mixin=True))
view.add_class(ClassDefinition('Bm1', is_a='Am1', mixin=True))
view.add_class(ClassDefinition('Bm2', is_a='Am2', mixin=True))
view.add_class(ClassDefinition('Cm1', is_a='Bm1', mixin=True))
view.add_class(ClassDefinition('Cm2', is_a='Bm2', mixin=True))
view.add_class(ClassDefinition('AZ', is_a='RootMixin', mixin=True))
view.add_class(ClassDefinition('BY', is_a='RootMixin', mixin=True))
view.add_class(ClassDefinition('CX', is_a='RootMixin', mixin=True))
def check(ancs: List, expected: List):
#print(ancs)
self.assertEqual(ancs, expected)
check(view.class_ancestors('C', depth_first=True),
['C', 'Cm1', 'Cm2', 'CX', 'B', 'Bm1', 'Bm2', 'BY', 'A', 'Am1', 'Am2', 'AZ', 'Root', 'RootMixin'])
check(view.class_ancestors('C', depth_first=False),
['C', 'Cm1', 'Cm2', 'CX', 'B', 'Bm1', 'Bm2', 'RootMixin', 'BY', 'A', 'Am1', 'Am2', 'AZ', 'Root'])
check(view.class_ancestors('C', mixins=False),
['C', 'B', 'A', 'Root'])
check(view.class_ancestors('C', is_a=False),
['C', 'Cm1', 'Cm2', 'CX'])



def test_slot_inheritance(self):
schema = SchemaDefinition(id='test', name='test')
view = SchemaView(schema)
Expand Down

0 comments on commit b27337b

Please sign in to comment.