diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py index f8d01daa..8b6fc90b 100644 --- a/linkml_runtime/utils/schemaview.py +++ b/linkml_runtime/utils/schemaview.py @@ -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 @@ -30,7 +30,7 @@ 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: @@ -38,14 +38,20 @@ def _closure(f, x, reflexive=True, **kwargs): 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): @@ -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 @@ -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]: @@ -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]: diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py index 034db716..b4264645 100644 --- a/tests/test_utils/test_schemaview.py +++ b/tests/test_utils/test_schemaview.py @@ -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 @@ -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)