Repository: linkml/linkml-runtime

Issue:
induce_slot returns incorrect range
In the NMDC Schema the `slot_usage` for [has input](https://github.com/microbiomedata/nmdc-schema/blob/d493f804d5aaed8b1c23e40e834a80e06308196a/src/schema/nmdc.yaml#L373) defines the range as `biosample`. However, `view.induced_slot('has input', 'biosample processing').range` returns `named thing`

yaml for `biosample processing`:
```
biosample processing:
    aliases:
      - material processing
    is_a: named thing
    description: >-
      A process that takes one or more biosamples as inputs and generates one or as outputs.
      Examples of outputs include samples cultivated from another sample or data objects created by instruments runs.
    slots:
      - has input
    slot_usage:
      has input:
        range: biosample
```

cc @cmungall 

Discussion:

Patch:
diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py
index 1e56887..ab1346d 100644
--- a/linkml_runtime/utils/schemaview.py
+++ b/linkml_runtime/utils/schemaview.py
@@ -132,7 +132,7 @@ class SchemaView(object):
         sname = self.importmap.get(str(imp), imp)               # Import map may use CURIE
         sname = self.namespaces().uri_for(sname) if ':' in sname else sname
         sname = self.importmap.get(str(sname), sname)               # It may also use URI or other forms
-        print(f'Loading schema {sname} from {from_schema.source_file}')
+        logging.info(f'Loading schema {sname} from {from_schema.source_file}')
         schema = load_schema_wrap(sname + '.yaml',
                                   base_dir=os.path.dirname(from_schema.source_file) if from_schema.source_file else None)
         return schema
@@ -667,8 +667,10 @@ class SchemaView(object):
             'minimum_value': lambda x, y: max(x, y),
         }
         for metaslot_name in SlotDefinition._inherited_slots:
+            # inheritance of slots; priority order
+            #   slot-level assignment < ancestor slot_usage < self slot_usage
             v = getattr(islot, metaslot_name, None)
-            for an in self.class_ancestors(class_name):
+            for an in reversed(self.class_ancestors(class_name)):
                 a = self.get_class(an, imports)
                 anc_slot_usage = a.slot_usage.get(slot_name, {})
                 v2 = getattr(anc_slot_usage, metaslot_name, None)
@@ -679,7 +681,9 @@ class SchemaView(object):
                         if v2 is not None:
                             v = COMBINE[metaslot_name](v, v2)
                     else:
-                        break
+                        if v2 is not None:
+                            v = v2
+                            logging.debug(f'{v} takes precedence over {v2} for {islot.name}.{metaslot_name}')
             if v is None:
                 if metaslot_name == 'range':
                     v = self.schema.default_range


Test patch:
diff --git a/tests/test_utils/input/kitchen_sink.yaml b/tests/test_utils/input/kitchen_sink.yaml
index 12bfd87..eaa79e1 100644
--- a/tests/test_utils/input/kitchen_sink.yaml
+++ b/tests/test_utils/input/kitchen_sink.yaml
@@ -223,7 +223,9 @@ slots:
       - subset A
       - subset B
   related to:
+    range: Thing
   type:
+    range: string
   street:
   city:
   has birth event:
diff --git a/tests/test_utils/input/kitchen_sink_noimports.yaml b/tests/test_utils/input/kitchen_sink_noimports.yaml
index 299f344..ee94381 100644
--- a/tests/test_utils/input/kitchen_sink_noimports.yaml
+++ b/tests/test_utils/input/kitchen_sink_noimports.yaml
@@ -250,7 +250,9 @@ slots:
       - subset A
       - subset B
   related to:
+    range: Thing
   type:
+    range: string
   street:
   city:
   has birth event:
diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py
index 9a3a974..6166e77 100644
--- a/tests/test_utils/test_schemaview.py
+++ b/tests/test_utils/test_schemaview.py
@@ -36,6 +36,8 @@ class SchemaViewTestCase(unittest.TestCase):
         #assert list(view.annotation_dict('employed at')[]
 
         if True:
+            for sn, s in view.all_slot().items():
+                logging.info(f'SN = {sn} RANGE={s.range}')
             # this section is mostly for debugging
             for cn in all_cls.keys():
                 logging.debug(f'{cn} PARENTS = {view.class_parents(cn)}')
@@ -86,9 +88,14 @@ class SchemaViewTestCase(unittest.TestCase):
         #assert view.get_class('Company').class_uri == 'prov:Agent'
         assert view.get_uri('Company') == 'ks:Company'
 
+        # test induced slots
+
         for c in ['Company', 'Person', 'Organization',]:
             assert view.induced_slot('aliases', c).multivalued is True
 
+        assert view.get_identifier_slot('Company').name == 'id'
+        assert view.get_identifier_slot('Thing').name == 'id'
+        assert view.get_identifier_slot('FamilialRelationship') is None
         for c in ['Company', 'Person', 'Organization', 'Thing']:
             assert view.induced_slot('id', c).identifier is True
             assert view.induced_slot('name', c).identifier is not True
@@ -99,10 +106,14 @@ class SchemaViewTestCase(unittest.TestCase):
             logging.debug(f's={s.range} // c = {c}')
             assert s.range == 'date'
             assert s.slot_uri == 'prov:startedAtTime'
+        # test slot_usage
         assert view.induced_slot('age in years', 'Person').minimum_value == 0
         assert view.induced_slot('age in years', 'Adult').minimum_value == 16
+        assert view.induced_slot('name', 'Person').pattern is not None
         assert view.induced_slot('type', 'FamilialRelationship').range == 'FamilialRelationshipType'
         assert view.induced_slot('related to', 'FamilialRelationship').range == 'Person'
+        assert view.get_slot('related to').range == 'Thing'
+        assert view.induced_slot('related to', 'Relationship').range == 'Thing'
 
         a = view.get_class('activity')
         self.assertCountEqual(a.exact_mappings, ['prov:Activity'])
@@ -213,7 +224,7 @@ class SchemaViewTestCase(unittest.TestCase):
             assert view.induced_slot('name', c).range == 'string'
         for c in ['Event', 'EmploymentEvent', 'MedicalEvent']:
             s = view.induced_slot('started at time', c)
-            logging.debug(f's={s.range} // c = {c}')
+            print(f's={s.range} // c = {c}')
             assert s.range == 'date'
             assert s.slot_uri == 'prov:startedAtTime'
         assert view.induced_slot('age in years', 'Person').minimum_value == 0