Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise an error when two classes have the same ambiguous attribute #352

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading