diff --git a/linkml_runtime/exceptions.py b/linkml_runtime/exceptions.py index 97a7bfbb..b42844b5 100644 --- a/linkml_runtime/exceptions.py +++ b/linkml_runtime/exceptions.py @@ -1,2 +1,10 @@ class OrderingError(RuntimeError): - """Exception raised when there is a problem with SchemaView ordering""" \ No newline at end of file + """Exception raised when there is a problem with SchemaView ordering""" + + +class AmbiguousNameError(RuntimeError): + """Exception raised in the case of an ambiguous element name""" + + +class MissingElementError(RuntimeError): + """Exception raised when an element is missing""" diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py index b937964d..e9115d90 100644 --- a/linkml_runtime/utils/schemaview.py +++ b/linkml_runtime/utils/schemaview.py @@ -15,7 +15,7 @@ from linkml_runtime.utils.formatutils import is_empty from linkml_runtime.utils.pattern import PatternResolver from linkml_runtime.linkml_model.meta import * -from linkml_runtime.exceptions import OrderingError +from linkml_runtime.exceptions import OrderingError, AmbiguousNameError, MissingElementError from enum import Enum logger = logging.getLogger(__name__) @@ -148,7 +148,6 @@ class SchemaView(object): # cached hash _hash: Optional[int] = None - def __init__(self, schema: Union[str, Path, SchemaDefinition], importmap: Optional[Dict[str, str]] = None, merge_imports: bool = False, base_dir: str = None): if isinstance(schema, Path): @@ -631,13 +630,16 @@ def get_slot(self, slot_name: SLOT_NAME, imports=True, attributes=True, strict=F for c in self.all_classes(imports=imports).values(): if slot_name in c.attributes: if slot is not None: - # slot name is ambiguous: return a stub slot - return SlotDefinition(slot_name) + raise AmbiguousNameError( + f'Attribute "{slot_name}" is already defined in another class, these attributes will be ' + f'ambiguous in RDF generators and you may need to rename them or restructure your schema. ' + f'Furthermore, you can use the induced_slot method with the slot name and its containing ' + f'class as arguments.') slot = copy(c.attributes[slot_name]) slot.from_schema = c.from_schema slot.owner = c.name if strict and slot is None: - raise ValueError(f'No such slot as "{slot_name}"') + raise MissingElementError(f'No such slot as "{slot_name}"') return slot @lru_cache(None) diff --git a/tests/test_utils/input/get_slot_with_ambiguous_attributes.yaml b/tests/test_utils/input/get_slot_with_ambiguous_attributes.yaml new file mode 100644 index 00000000..0108dfc0 --- /dev/null +++ b/tests/test_utils/input/get_slot_with_ambiguous_attributes.yaml @@ -0,0 +1,36 @@ +id: https://examples.org/get-slot-with-attribute# +name: get-slot-with-attribute + +prefixes: + test: https://examples.org/get-slot-with-attribute# + +default_prefix: test +default_range: string + +slots: + uniqueSlot: + description: "A unique slot just to test that get_slot still works when attributes are ignored in this schema" + range: string + +classes: + ClassWithAttributes: + slots: + - uniquesSlot + attributes: + randomAttribute: + description: "A random attribute for testing purposes" + range: integer + minimum_value: 0 + maximum_value: 999 + + ImportantSecondClass: + description: "Important class to reproduce the error I got as the class loop needs to have at least a + second iteration" + slots: + - uniqueSlot + attributes: + randomAttribute: + description: "Now you see the ambiguity intensifying ?" + range: integer + minimum_value: 0 + maximum_value: 111 \ No newline at end of file diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py index c9f28ff8..a5e685b0 100644 --- a/tests/test_utils/test_schemaview.py +++ b/tests/test_utils/test_schemaview.py @@ -6,6 +6,7 @@ from jsonasobj2 import JsonObj from linkml_runtime.dumpers import yaml_dumper +from linkml_runtime.exceptions import AmbiguousNameError, MissingElementError from linkml_runtime.linkml_model.meta import Example, SchemaDefinition, ClassDefinition, SlotDefinitionName, SlotDefinition, \ ClassDefinitionName, Prefix from linkml_runtime.loaders.yaml_loader import YAMLLoader @@ -701,7 +702,7 @@ def test_slot_inheritance(): # Test dangling view.add_slot(SlotDefinition('s5', is_a='does-not-exist')) - with pytest.raises(ValueError): + with pytest.raises(MissingElementError): view.slot_ancestors('s5') def test_attribute_inheritance(): @@ -744,9 +745,6 @@ def test_ambiguous_attributes(): a2x = SlotDefinition('a2', range='BarEnum') view.add_class(ClassDefinition('C2', attributes={a1x.name: a1x, a2x.name: a2x})) - assert view.get_slot(a1.name).range is None - assert view.get_slot(a2.name).range is None - assert view.get_slot(a3.name).range is not None assert len(view.all_slots(attributes=True)) == 3 assert len(view.all_slots(attributes=False)) == 0 assert len(view.all_slots()) == 3 @@ -757,6 +755,21 @@ def test_ambiguous_attributes(): assert view.induced_slot(a2x.name, 'C2').range == a2x.range +def test_ambiguous_attribute_through_get_slot(): + schema_path = os.path.join(INPUT_DIR, "get_slot_with_ambiguous_attributes.yaml") + sv = SchemaView(schema_path) + + assert sv.get_slot("uniqueSlot") is not None + assert sv.get_slot("randomAttribute", attributes=False) is None + assert sv.induced_slot("uniqueSlot", "ImportantSecondClass") is not None + + with pytest.raises(AmbiguousNameError) as exception: + sv.get_slot("randomAttribute") + assert str(exception.value) == ('Attribute "randomAttribute" is already defined in another class, ' + 'these attributes will be ambiguous in RDF generators and you may need to rename ' + 'them or restructure your schema. Furthermore, you can use the induced_slot ' + 'method with the slot name and its containing class as arguments.') + def test_metamodel_in_schemaview(): view = package_schemaview('linkml_runtime.linkml_model.meta') assert 'meta' in view.imports_closure()