Skip to content

Commit

Permalink
raise an error when two classes have the same ambiguous attribute
Browse files Browse the repository at this point in the history
Closes linkml/linkml#2403

Signed-off-by: Vincent Kelleher <[email protected]>
  • Loading branch information
Vincent Kelleher committed Dec 23, 2024
1 parent ad74966 commit 1dd9f6f
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 10 deletions.
10 changes: 9 additions & 1 deletion linkml_runtime/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
class OrderingError(RuntimeError):
"""Exception raised when there is a problem with SchemaView ordering"""
"""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"""
12 changes: 7 additions & 5 deletions linkml_runtime/utils/schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions tests/test_utils/input/get_slot_with_ambiguous_attributes.yaml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 17 additions & 4 deletions tests/test_utils/test_schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down

0 comments on commit 1dd9f6f

Please sign in to comment.