Repository: linkml/linkml

Issue:
personinfo.yaml: CHILD_OF should be famrel:03
https://github.com/linkml/linkml/blob/main/examples/PersonSchema/personinfo.yaml#L252:
`CHILD_OF meaning: famrel:01` conflicts with SIBLING_OF. 
Should be eg `famrel:03`

Discussion:

Patch:
diff --git a/examples/PersonSchema/personinfo.yaml b/examples/PersonSchema/personinfo.yaml
index 78b68fff..4cc629eb 100644
--- a/examples/PersonSchema/personinfo.yaml
+++ b/examples/PersonSchema/personinfo.yaml
@@ -249,7 +249,7 @@ enums:
       PARENT_OF:
         meaning: famrel:02
       CHILD_OF:
-        meaning: famrel:01
+        meaning: famrel:03
   GenderType:
     permissible_values:
       nonbinary man:
diff --git a/linkml/generators/docgen.py b/linkml/generators/docgen.py
index efffd98b..7031199f 100644
--- a/linkml/generators/docgen.py
+++ b/linkml/generators/docgen.py
@@ -370,6 +370,14 @@ class DocGenerator(Generator):
             return self._markdown_link(camelcase(e.name))
         else:
             return e.name
+        
+    def links(self, e_list: List[DefinitionName]) -> List[str]:
+        """Render list of element documentation pages as hyperlinks.
+
+        :param e_list: list of elements
+        :return: list of hyperlinked elements
+        """
+        return list(map(self.link, e_list))
 
     def _exclude_type(self, t: TypeDefinition) -> bool:
         return self._is_external(t) and not self.schemaview.schema.id.startswith(
@@ -727,7 +735,19 @@ class DocGenerator(Generator):
         slot_list = [slot.name for slot in sv.class_induced_slots(class_name=cls.name)]
 
         return list(set(slot_list).difference(self.get_direct_slots(cls)))
+    
+    def get_slot_inherited_from(self, class_name: ClassDefinitionName, slot_name: SlotDefinitionName) -> List[ClassDefinitionName]:
+        """Get the name of the class that a given slot is inherited from.
 
+        :param class_name: name of the class whose slot we are checking
+        :param slot_name: name of slot in consideration
+        :return: list of classes
+        """
+        sv = self.schemaview
+        induced_slot = sv.induced_slot(slot_name, class_name)
+        ancestors = sv.class_ancestors(class_name)
+        return list(set(induced_slot.domain_of).intersection(ancestors))
+    
     def get_mixin_inherited_slots(self, cls: ClassDefinition) -> Dict[str, List[str]]:
         """Fetch list of all slots acquired through mixing.
 
diff --git a/linkml/generators/docgen/class.md.jinja2 b/linkml/generators/docgen/class.md.jinja2
index 011d5850..954f779a 100644
--- a/linkml/generators/docgen/class.md.jinja2
+++ b/linkml/generators/docgen/class.md.jinja2
@@ -32,17 +32,16 @@ URI: {{ gen.uri_link(element) }}
 
 | Name | Cardinality and Range | Description | Inheritance |
 | ---  | --- | --- | --- |
-{% for s in schemaview.class_induced_slots(element.name) -%}
-{% if s.name in gen.get_direct_slots(element) -%}
-| {{gen.link(s)}} | {{ gen.cardinality(s) }} <br/> {{ gen.link(s.range) }} | {{ s.description }}  | direct |
+{%- if gen.get_direct_slots(element)|length > 0 %}
+{% for d_slot in gen.get_direct_slots(element) -%}
+| {{ gen.link(schemaview.get_slot(d_slot)) }} | {{ gen.cardinality(schemaview.get_slot(d_slot)) }} <br/> {{ schemaview.get_slot(d_slot).range }} | {{ schemaview.get_slot(d_slot).description }} | direct |
+{% endfor -%}
 {% endif -%}
-{%- if s.name in gen.get_indirect_slots(element) -%}
-| {{gen.link(s)}} | {{ gen.cardinality(s) }} <br/> {{ gen.link(s.range) }} | {{ s.description }}  | inherited |
+{% if gen.get_indirect_slots(element)|length > 0 -%}
+{% for i_slot in gen.get_indirect_slots(element) -%}
+| {{ gen.link(schemaview.get_slot(i_slot)) }} | {{ gen.cardinality(schemaview.get_slot(i_slot)) }} <br/> {{ schemaview.get_slot(i_slot).range }} | {{ schemaview.get_slot(i_slot).description }} | {{ gen.links(gen.get_slot_inherited_from(element.name, i_slot))|join(', ') }} |
+{% endfor -%}
 {% endif -%}
-{%- if s.name in gen.get_mixin_inherited_slots(element).items() -%}
-| {{gen.link(s)}} | {{ gen.cardinality(s) }} <br/> {{ gen.link(s.range) }} | {{ s.description }}  | mixin |
-{% endif -%}
-{%- endfor %}
 
 {% if schemaview.usage_index().get(element.name) %}
 ## Usages
@@ -64,7 +63,7 @@ URI: {{ gen.uri_link(element) }}
 | ---  | ---  |
 {% for m, mt in schemaview.get_mappings(element.name).items() -%}
 {% if mt|length > 0 -%}
-| {{ m }} | {{ mt }}|join(', ') |
+| {{ m }} | {{ mt|join(', ') }} |
 {% endif -%}
 {% endfor %}
 


Test patch:
diff --git a/tests/test_generators/test_docgen.py b/tests/test_generators/test_docgen.py
index 5387018a..a7aa4a79 100644
--- a/tests/test_generators/test_docgen.py
+++ b/tests/test_generators/test_docgen.py
@@ -415,7 +415,7 @@ class DocGeneratorTestCase(unittest.TestCase):
         actual_result = gen.get_indirect_slots(cls)
         expected_result = ["ended at time", "metadata", "started at time", "is current"]
         
-        self.assertListEqual(sorted(expected_result), sorted(actual_result))
+        self.assertCountEqual(expected_result, actual_result)
 
         # test assertion for mixed in slots of a class
         cls = sv.get_class("Organization")
@@ -424,6 +424,18 @@ class DocGeneratorTestCase(unittest.TestCase):
 
         self.assertDictEqual(actual_result, expected_result)
 
+    def test_class_slots_inheritance(self):
+        gen = DocGenerator(SCHEMA, mergeimports=True, no_types_dir=True)
+
+        sv = SchemaView(SCHEMA)
+        test_class = sv.get_class("BirthEvent")
+        test_slot = sv.get_slot("started at time")
+
+        expected_result = ["Event"]
+        actual_result = gen.get_slot_inherited_from(class_name=test_class.name, slot_name=test_slot.name)
+
+        self.assertListEqual(expected_result, actual_result)
+
     def test_use_slot_uris(self):
         tdir = env.input_path("docgen_html_templates")
         gen = DocGenerator(SCHEMA, mergeimports=True, no_types_dir=True, template_directory=tdir, use_slot_uris=True)