Skip to content

Commit 1dd9f6f

Browse files
author
Vincent Kelleher
committed
raise an error when two classes have the same ambiguous attribute
Closes linkml/linkml#2403 Signed-off-by: Vincent Kelleher <[email protected]>
1 parent ad74966 commit 1dd9f6f

File tree

4 files changed

+69
-10
lines changed

4 files changed

+69
-10
lines changed

linkml_runtime/exceptions.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
class OrderingError(RuntimeError):
2-
"""Exception raised when there is a problem with SchemaView ordering"""
2+
"""Exception raised when there is a problem with SchemaView ordering"""
3+
4+
5+
class AmbiguousNameError(RuntimeError):
6+
"""Exception raised in the case of an ambiguous element name"""
7+
8+
9+
class MissingElementError(RuntimeError):
10+
"""Exception raised when an element is missing"""

linkml_runtime/utils/schemaview.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from linkml_runtime.utils.formatutils import is_empty
1616
from linkml_runtime.utils.pattern import PatternResolver
1717
from linkml_runtime.linkml_model.meta import *
18-
from linkml_runtime.exceptions import OrderingError
18+
from linkml_runtime.exceptions import OrderingError, AmbiguousNameError, MissingElementError
1919
from enum import Enum
2020

2121
logger = logging.getLogger(__name__)
@@ -148,7 +148,6 @@ class SchemaView(object):
148148
# cached hash
149149
_hash: Optional[int] = None
150150

151-
152151
def __init__(self, schema: Union[str, Path, SchemaDefinition],
153152
importmap: Optional[Dict[str, str]] = None, merge_imports: bool = False, base_dir: str = None):
154153
if isinstance(schema, Path):
@@ -631,13 +630,16 @@ def get_slot(self, slot_name: SLOT_NAME, imports=True, attributes=True, strict=F
631630
for c in self.all_classes(imports=imports).values():
632631
if slot_name in c.attributes:
633632
if slot is not None:
634-
# slot name is ambiguous: return a stub slot
635-
return SlotDefinition(slot_name)
633+
raise AmbiguousNameError(
634+
f'Attribute "{slot_name}" is already defined in another class, these attributes will be '
635+
f'ambiguous in RDF generators and you may need to rename them or restructure your schema. '
636+
f'Furthermore, you can use the induced_slot method with the slot name and its containing '
637+
f'class as arguments.')
636638
slot = copy(c.attributes[slot_name])
637639
slot.from_schema = c.from_schema
638640
slot.owner = c.name
639641
if strict and slot is None:
640-
raise ValueError(f'No such slot as "{slot_name}"')
642+
raise MissingElementError(f'No such slot as "{slot_name}"')
641643
return slot
642644

643645
@lru_cache(None)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
id: https://examples.org/get-slot-with-attribute#
2+
name: get-slot-with-attribute
3+
4+
prefixes:
5+
test: https://examples.org/get-slot-with-attribute#
6+
7+
default_prefix: test
8+
default_range: string
9+
10+
slots:
11+
uniqueSlot:
12+
description: "A unique slot just to test that get_slot still works when attributes are ignored in this schema"
13+
range: string
14+
15+
classes:
16+
ClassWithAttributes:
17+
slots:
18+
- uniquesSlot
19+
attributes:
20+
randomAttribute:
21+
description: "A random attribute for testing purposes"
22+
range: integer
23+
minimum_value: 0
24+
maximum_value: 999
25+
26+
ImportantSecondClass:
27+
description: "Important class to reproduce the error I got as the class loop needs to have at least a
28+
second iteration"
29+
slots:
30+
- uniqueSlot
31+
attributes:
32+
randomAttribute:
33+
description: "Now you see the ambiguity intensifying ?"
34+
range: integer
35+
minimum_value: 0
36+
maximum_value: 111

tests/test_utils/test_schemaview.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from jsonasobj2 import JsonObj
77

88
from linkml_runtime.dumpers import yaml_dumper
9+
from linkml_runtime.exceptions import AmbiguousNameError, MissingElementError
910
from linkml_runtime.linkml_model.meta import Example, SchemaDefinition, ClassDefinition, SlotDefinitionName, SlotDefinition, \
1011
ClassDefinitionName, Prefix
1112
from linkml_runtime.loaders.yaml_loader import YAMLLoader
@@ -701,7 +702,7 @@ def test_slot_inheritance():
701702

702703
# Test dangling
703704
view.add_slot(SlotDefinition('s5', is_a='does-not-exist'))
704-
with pytest.raises(ValueError):
705+
with pytest.raises(MissingElementError):
705706
view.slot_ancestors('s5')
706707

707708
def test_attribute_inheritance():
@@ -744,9 +745,6 @@ def test_ambiguous_attributes():
744745
a2x = SlotDefinition('a2', range='BarEnum')
745746
view.add_class(ClassDefinition('C2', attributes={a1x.name: a1x, a2x.name: a2x}))
746747

747-
assert view.get_slot(a1.name).range is None
748-
assert view.get_slot(a2.name).range is None
749-
assert view.get_slot(a3.name).range is not None
750748
assert len(view.all_slots(attributes=True)) == 3
751749
assert len(view.all_slots(attributes=False)) == 0
752750
assert len(view.all_slots()) == 3
@@ -757,6 +755,21 @@ def test_ambiguous_attributes():
757755
assert view.induced_slot(a2x.name, 'C2').range == a2x.range
758756

759757

758+
def test_ambiguous_attribute_through_get_slot():
759+
schema_path = os.path.join(INPUT_DIR, "get_slot_with_ambiguous_attributes.yaml")
760+
sv = SchemaView(schema_path)
761+
762+
assert sv.get_slot("uniqueSlot") is not None
763+
assert sv.get_slot("randomAttribute", attributes=False) is None
764+
assert sv.induced_slot("uniqueSlot", "ImportantSecondClass") is not None
765+
766+
with pytest.raises(AmbiguousNameError) as exception:
767+
sv.get_slot("randomAttribute")
768+
assert str(exception.value) == ('Attribute "randomAttribute" is already defined in another class, '
769+
'these attributes will be ambiguous in RDF generators and you may need to rename '
770+
'them or restructure your schema. Furthermore, you can use the induced_slot '
771+
'method with the slot name and its containing class as arguments.')
772+
760773
def test_metamodel_in_schemaview():
761774
view = package_schemaview('linkml_runtime.linkml_model.meta')
762775
assert 'meta' in view.imports_closure()

0 commit comments

Comments
 (0)