Skip to content
This repository was archived by the owner on Apr 16, 2026. It is now read-only.

Commit 9d191ad

Browse files
Merge pull request #464 from linkml/schemaview_identifier_key_tests
schemaview.py: add tests for get_identifier_slot and get_key_slot
2 parents 9be4b3e + 83e34c9 commit 9d191ad

2 files changed

Lines changed: 150 additions & 27 deletions

File tree

‎linkml_runtime/utils/schemaview.py‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,31 +1451,6 @@ def annotation_dict(self, element_name: ElementName, imports: bool = True) -> di
14511451
e = self.get_element(element_name, imports=imports)
14521452
return {k: v.value for k, v in e.annotations.items()}
14531453

1454-
@lru_cache(None)
1455-
def class_slots(
1456-
self, class_name: CLASS_NAME, imports: bool = True, direct: bool = False, attributes: bool = True
1457-
) -> list[SlotDefinitionName]:
1458-
"""Return all slots for a class.
1459-
1460-
:param class_name:
1461-
:param imports: include imports closure
1462-
:param direct: only returns slots directly associated with a class (default is False)
1463-
:param attributes: include attribute declarations as well as slots (default is True)
1464-
:return: all slot names applicable for a class
1465-
"""
1466-
ancs = [class_name] if direct else self.class_ancestors(class_name, imports=imports)
1467-
slots = []
1468-
for an in ancs:
1469-
a = self.get_class(an, imports)
1470-
slots += a.slots
1471-
if attributes:
1472-
slots += a.attributes.keys()
1473-
slots_nr = []
1474-
for s in slots:
1475-
if s not in slots_nr:
1476-
slots_nr.append(s)
1477-
return slots_nr
1478-
14791454
@lru_cache(None)
14801455
def induced_slot(
14811456
self,
@@ -1587,6 +1562,31 @@ def _metaslots_for_slot(self):
15871562
fake_slot = SlotDefinition("__FAKE")
15881563
return vars(fake_slot).keys()
15891564

1565+
@lru_cache(None)
1566+
def class_slots(
1567+
self, class_name: CLASS_NAME, imports: bool = True, direct: bool = False, attributes: bool = True
1568+
) -> list[SlotDefinitionName]:
1569+
"""Return all slots for a class.
1570+
1571+
:param class_name:
1572+
:param imports: include imports closure
1573+
:param direct: only returns slots directly associated with a class (default is False)
1574+
:param attributes: include attribute declarations as well as slots (default is True)
1575+
:return: all slot names applicable for a class
1576+
"""
1577+
ancs = [class_name] if direct else self.class_ancestors(class_name, imports=imports)
1578+
slots = []
1579+
for an in ancs:
1580+
a = self.get_class(an, imports)
1581+
slots += a.slots
1582+
if attributes and a.attributes:
1583+
slots += a.attributes.keys()
1584+
slots_nr = []
1585+
for s in slots:
1586+
if s not in slots_nr:
1587+
slots_nr.append(s)
1588+
return slots_nr
1589+
15901590
@lru_cache(None)
15911591
def class_induced_slots(self, class_name: CLASS_NAME | None = None, imports: bool = True) -> list[SlotDefinition]:
15921592
"""Retrieve all slots that are asserted or inferred for a class, with their inferred semantics.
@@ -1651,7 +1651,7 @@ def get_identifier_slot(self, cn: CLASS_NAME, use_key: bool = False, imports: bo
16511651
:param imports:
16521652
:return: slot that acts as identifier, if present
16531653
"""
1654-
for sn in self.class_slots(cn, imports=imports):
1654+
for sn in sorted(self.class_slots(cn, imports=imports)):
16551655
s = self.induced_slot(sn, cn, imports=imports)
16561656
if s.identifier:
16571657
return s
@@ -1667,7 +1667,7 @@ def get_key_slot(self, cn: CLASS_NAME, imports: bool = True) -> SlotDefinition |
16671667
:param imports:
16681668
:return: slot that acts as key, if present
16691669
"""
1670-
for sn in self.class_slots(cn, imports=imports):
1670+
for sn in sorted(self.class_slots(cn, imports=imports)):
16711671
s = self.induced_slot(sn, cn, imports=imports)
16721672
if s.key:
16731673
return s

‎tests/test_utils/test_schemaview.py‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@
7070
RELATED_TO = "related to"
7171
AGE_IN_YEARS = "age in years"
7272

73+
EMPTY = ""
74+
ID = "identifier"
75+
KEY = "key"
76+
7377
ALL_ELEMENTS = [PREFIXES, *SCHEMA_ELEMENTS]
7478
EXPECTED = {
7579
PREFIXES: {"sc1p1", "sc2p1"},
@@ -1631,6 +1635,125 @@ def test_type_and_slot_with_same_name() -> None:
16311635
assert view.get_uri("test", imports=True) == "ex:test"
16321636

16331637

1638+
@pytest.fixture(scope="module")
1639+
def identifier_key_schema() -> str:
1640+
"""Return a schema that defines a set of slots with identifiers and keys, and a test class to populate."""
1641+
return """
1642+
# yaml-language-server: $schema=https://linkml.io/linkml-model/linkml_model/jsonschema/meta.schema.json
1643+
id: https://w3id.org/linkml/examples/identifiers
1644+
title: Identifiers and Keys
1645+
name: identifiers
1646+
description: Schema for testing identifiers and keys
1647+
slots:
1648+
slot_a:
1649+
slot_b:
1650+
id_slot_a:
1651+
identifier: true
1652+
id_slot_b:
1653+
identifier: true
1654+
id_slot_false:
1655+
identifier: false
1656+
key_slot_a:
1657+
key: true
1658+
key_slot_b:
1659+
key: true
1660+
key_slot_false:
1661+
key: false
1662+
1663+
classes:
1664+
TestClass:
1665+
slots:
1666+
- slot_a
1667+
- slot_b
1668+
"""
1669+
1670+
1671+
@pytest.mark.parametrize("id_slot_a", [EMPTY, "id_slot_a"])
1672+
@pytest.mark.parametrize("id_slot_b", [EMPTY, "id_slot_b"])
1673+
@pytest.mark.parametrize("id_slot_false", [EMPTY, "id_slot_false"])
1674+
@pytest.mark.parametrize("key_slot_a", [EMPTY, "key_slot_a"])
1675+
@pytest.mark.parametrize("key_slot_b", [EMPTY, "key_slot_b"])
1676+
@pytest.mark.parametrize("key_slot_false", [EMPTY, "key_slot_false"])
1677+
def test_get_identifier_get_key_slot(
1678+
identifier_key_schema: str,
1679+
id_slot_a: str,
1680+
id_slot_b: str,
1681+
id_slot_false: str,
1682+
key_slot_a: str,
1683+
key_slot_b: str,
1684+
key_slot_false: str,
1685+
) -> None:
1686+
"""Test the retrieval of "identifier" and "key" slots in classes in the schema.
1687+
1688+
The parameters represent the slots to be added to the test class; if the value of the parameter is EMPTY,
1689+
the slot is not added. If the parameter is a string, that slot is added to the test class.
1690+
1691+
For example, given the following set of parameters:
1692+
1693+
id_slot_a: "id_slot_a"
1694+
id_slot_b: EMPTY
1695+
id_slot_false: "id_slot_false"
1696+
key_slot_a: EMPTY
1697+
key_slot_b: EMPTY
1698+
key_slot_false: "key_slot_false"
1699+
1700+
The following lines would be added to the bottom of the identifier_key_schema:
1701+
1702+
- id_slot_a
1703+
- id_slot_false
1704+
- key_slot_false
1705+
1706+
resulting in the following structure for `TestClass`:
1707+
1708+
classes:
1709+
TestClass:
1710+
slots:
1711+
- slot_a
1712+
- slot_b
1713+
- id_slot_a
1714+
- id_slot_false
1715+
- key_slot_false
1716+
1717+
This allows the testing of a large number of combinations without having to generate all 2^^6 (64) manually.
1718+
"""
1719+
all_id_slots = [id_slot_a, id_slot_b, id_slot_false]
1720+
all_key_slots = [key_slot_a, key_slot_b, key_slot_false]
1721+
1722+
for k in [*all_id_slots, *all_key_slots]:
1723+
if k:
1724+
identifier_key_schema += f" - {k}\n"
1725+
1726+
sv = SchemaView(identifier_key_schema)
1727+
test_class_slots = sv.class_slots("TestClass")
1728+
# ensure that the correct slots are in the schema
1729+
assert set(test_class_slots) == {"slot_a", "slot_b", *[s for s in [*all_id_slots, *all_key_slots] if s]}
1730+
1731+
# Generate the expected slots to be returned by `get_***_slot` for identifiers and keys.
1732+
# Note that id_slot_false and key_slot_false have `identifier` and `key` set to `false`,
1733+
# so we would not expect `get_identifier_slot` or `get_key_slot` to return these slots.
1734+
# If the value from the parameters is EMPTY, the attribute is not present.
1735+
expected = {
1736+
# expected result of get_identifier_slot
1737+
ID: id_slot_a or id_slot_b,
1738+
# expected result of get_identifier_slot, falling back on get_key_slot
1739+
f"{ID}_or_{KEY}": id_slot_a or id_slot_b or key_slot_a or key_slot_b,
1740+
# expected result of get_key_slot
1741+
KEY: key_slot_a or key_slot_b,
1742+
}
1743+
1744+
got = {
1745+
ID: sv.get_identifier_slot("TestClass", use_key=False),
1746+
f"{ID}_or_{KEY}": sv.get_identifier_slot("TestClass", use_key=True),
1747+
KEY: sv.get_key_slot("TestClass"),
1748+
}
1749+
1750+
for id_key, exp in expected.items():
1751+
if exp:
1752+
assert got[id_key].name == exp
1753+
else:
1754+
assert got[id_key] is None
1755+
1756+
16341757
"""
16351758
merge_schema tests: https://github.com/linkml/linkml/issues/1143
16361759
"""

0 commit comments

Comments
 (0)