Skip to content

Commit

Permalink
fix induced_slots tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sierra-moxon committed Oct 2, 2024
1 parent 97f91f6 commit e206916
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 20 deletions.
84 changes: 64 additions & 20 deletions linkml_runtime/utils/schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
import uuid
import logging
import collections
from dataclasses import fields
from functools import lru_cache
from copy import copy, deepcopy
from collections import defaultdict, deque
from pathlib import Path
from pprint import pprint
from typing import Mapping, Optional, Tuple, TypeVar
import warnings

Expand All @@ -17,6 +19,8 @@
from linkml_runtime.linkml_model.meta import *
from linkml_runtime.exceptions import OrderingError
from enum import Enum
from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition, ClassDefinitionName
from dataclasses import asdict, is_dataclass, fields

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -76,6 +80,40 @@ def _closure(f, x, reflexive=True, depth_first=True, **kwargs):
return rv


def to_dict(obj):
"""
Convert a LinkML element (such as ClassDefinition) to a dictionary.
:param obj: The LinkML class instance to convert.
:return: A dictionary representation of the class.
"""
if is_dataclass(obj):
return asdict(obj)
elif isinstance(obj, list):
return [to_dict(item) for item in obj]
elif isinstance(obj, dict):
return {key: to_dict(value) for key, value in obj.items()}
else:
return obj

Check warning on line 96 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L96

Added line #L96 was not covered by tests


def get_anonymous_class_definition(class_as_dict: ClassDefinition) -> AnonymousClassExpression:
"""
Convert a ClassDefinition to an AnonymousClassExpression, typically for use in defining an Expression object
(e.g. SlotDefinition.range_expression). This method only fills out the fields that are present in the
AnonymousClassExpression class. #TODO: We should consider whether an Expression should share a common ancestor with
the Definition classes.
:param class_as_dict: The ClassDefinition to convert.
:return: An AnonymousClassExpression.
"""
an_expr = AnonymousClassExpression()
valid_fields = {field.name for field in fields(an_expr)}
for k, v in class_as_dict.items():
if k in valid_fields:
setattr(an_expr, k, v)
for k, v in class_as_dict.items():
setattr(an_expr, k, v)
return an_expr

def load_schema_wrap(path: str, **kwargs):
# import here to avoid circular imports
from linkml_runtime.loaders.yaml_loader import YAMLLoader
Expand Down Expand Up @@ -1375,7 +1413,6 @@ def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, impo
}
# iterate through all metaslots, and potentially populate metaslot value for induced slot
for metaslot_name in self._metaslots_for_slot():

# inheritance of slots; priority order
# slot-level assignment < ancestor slot_usage < self slot_usage
v = getattr(induced_slot, metaslot_name, None)
Expand All @@ -1398,30 +1435,31 @@ def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, impo
if metaslot_name in ["any_of", "exactly_one_of"]:
if anc_slot_usage != {}:
for ao in anc_slot_usage.any_of:
ao_acd = None
if ao.range is not None:
ao_range = self.get_element(ao.range)
if ao_range:
print(ao_range)
acd = get_anonymous_class_definition(to_dict(ao_range))
ao_acd = get_anonymous_class_definition(to_dict(ao_range))
if induced_slot.range_expression is None:
induced_slot.range_expression = AnonymousClassExpression()
if induced_slot.range_expression.any_of is None:
induced_slot.range_expression.any_of = []

Check warning on line 1446 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L1446

Added line #L1446 was not covered by tests
# Check for duplicates before appending
if acd not in induced_slot.range_expression.any_of:
induced_slot.range_expression.any_of.append(acd)
if ao_acd is not None and ao_acd not in induced_slot.range_expression.any_of:
induced_slot.range_expression.any_of.append(ao_acd)
for eoo in anc_slot_usage.exactly_one_of:
eoo_acd = None

Check warning on line 1451 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L1451

Added line #L1451 was not covered by tests
if eoo.range is not None:
eoo_range = self.get_element(eoo.range)
print(eoo_range)
acd = get_anonymous_class_definition(as_dict(eoo_range))
eoo_range = self.get_class(eoo.range)

Check warning on line 1453 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L1453

Added line #L1453 was not covered by tests
if eoo_range is not None:
eoo_acd = get_anonymous_class_definition(as_dict(eoo_range))

Check warning on line 1455 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L1455

Added line #L1455 was not covered by tests
if induced_slot.range_expression is None:
induced_slot.range_expression = AnonymousClassExpression()

Check warning on line 1457 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L1457

Added line #L1457 was not covered by tests
if induced_slot.range_expression.exactly_one_of is None:
induced_slot.range_expression.exactly_one_of = []

Check warning on line 1459 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L1459

Added line #L1459 was not covered by tests
# Check for duplicates before appending
if acd not in induced_slot.range_expression.exactly_one_of:
induced_slot.range_expression.exactly_one_of.append(acd)
if eoo_acd is not None and eoo_acd not in induced_slot.range_expression.exactly_one_of:
induced_slot.range_expression.exactly_one_of.append(eoo_acd)

Check warning on line 1462 in linkml_runtime/utils/schemaview.py

View check run for this annotation

Codecov / codecov/patch

linkml_runtime/utils/schemaview.py#L1462

Added line #L1462 was not covered by tests
if v is None:
v = v2
else:
Expand Down Expand Up @@ -1458,28 +1496,34 @@ def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, impo
if c.name not in induced_slot.domain_of:
induced_slot.domain_of.append(c.name)
if induced_slot.range is not None:
pprint(induced_slot)
if induced_slot.range_expression is None:
induced_slot.range_expression = AnonymousClassExpression()
induced_slot.range_expression.any_of = []
induced_slot.range_expression.any_of.append(
get_anonymous_class_definition(to_dict(self.get_element(induced_slot.range)))
)
range_class = self.get_class(induced_slot.range)
if range_class is not None:
induced_slot.range_expression.any_of.append(
get_anonymous_class_definition(to_dict(range_class))
)
return induced_slot
else:
any_of_ancestors = []
if induced_slot.range_expression.any_of is not None:
for ao_range in induced_slot.range_expression.any_of:
ao_range_class = self.get_class(ao_range.name)
ao_anc = self.class_ancestors(ao_range_class.name)
for a in ao_anc:
if a not in any_of_ancestors:
any_of_ancestors.append(a)
if ao_range_class is not None:
ao_anc = self.class_ancestors(ao_range_class.name)
for a in ao_anc:
if a not in any_of_ancestors:
any_of_ancestors.append(a)
if induced_slot.range in any_of_ancestors:
return induced_slot
else:
induced_slot.range_expression.any_of.append(
get_anonymous_class_definition(to_dict(self.get_class(induced_slot.range)))
)
range_class = self.get_class(induced_slot.range)
if range_class is not None:
induced_slot.range_expression.any_of.append(
get_anonymous_class_definition(to_dict(range_class))
)
return induced_slot
return induced_slot

Expand Down
3 changes: 3 additions & 0 deletions tests/test_utils/input/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ types:
SymbolString:
typeof: string

NarrativeText:
typeof: string

classes:

activity:
Expand Down
45 changes: 45 additions & 0 deletions tests/test_utils/input/kitchen_sink.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,49 @@ classes:
- id
- name

Alien:
is_a: Thing
slots:
- height_in_m
- type
- related to
slot_usage:
type:
range: OrganismType
required: true
related to:
any_of:
- range: Person
- range: Alien
range: FamilialRelationship
id_prefixes: [ ks ]

Martian:
is_a: Thing
slots:
- height_in_m
- type
- related to
slot_usage:
type:
range: OrganismType
required: true
related to:
range: Alien
required: true

Venetian:
is_a: Thing
slots:
- height_in_m
- type
- related to
slot_usage:
related to:
any_of:
- range: Person
- range: Alien

Person:
is_a: Thing
in_subset:
Expand Down Expand Up @@ -225,6 +268,8 @@ slots:
- subset B
related to:
range: Thing
associated with:
range: Thing
type:
range: string
street:
Expand Down
1 change: 1 addition & 0 deletions tests/test_utils/test_schemaview.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def test_alias_slot():

for c in view.all_classes().values():
for s in view.class_induced_slots(c.name):
print("slot: ", s)
assert s.alias is not None # Assert that alias is not None

postal_code_slot = view.induced_slot('postal code', 'Address')
Expand Down

0 comments on commit e206916

Please sign in to comment.