diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 38b10dcf..45a5af05 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -33,7 +33,7 @@ jobs: # check-out repo and set-up python #---------------------------------------------- - name: Check out repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -66,7 +66,7 @@ jobs: # upload coverage results #---------------------------------------------- - name: Upload coverage report - uses: codecov/codecov-action@v1.0.5 + uses: codecov/codecov-action@v3 with: name: codecov-results-${{ matrix.os }}-${{ matrix.python-version }} token: ${{ secrets.CODECOV_TOKEN }} diff --git a/linkml_runtime/linkml_model/meta.py b/linkml_runtime/linkml_model/meta.py index 18261d4f..22c57922 100644 --- a/linkml_runtime/linkml_model/meta.py +++ b/linkml_runtime/linkml_model/meta.py @@ -873,7 +873,8 @@ class EnumDefinition(Definition): code_set_tag: Optional[str] = None code_set_version: Optional[str] = None pv_formula: Optional[Union[str, "PvFormulaOptions"]] = None - permissible_values: Optional[Union[Dict[Union[str, PermissibleValueText], Union[dict, "PermissibleValue"]], List[Union[dict, "PermissibleValue"]]]] = empty_dict() + permissible_values: Optional[Union[Dict[Union[str, PermissibleValueText], Union[dict, "PermissibleValue"]], + List[Union[dict, "PermissibleValue"]]]] = empty_dict() include: Optional[Union[Union[dict, AnonymousEnumExpression], List[Union[dict, AnonymousEnumExpression]]]] = empty_list() minus: Optional[Union[Union[dict, AnonymousEnumExpression], List[Union[dict, AnonymousEnumExpression]]]] = empty_list() inherits: Optional[Union[Union[str, EnumDefinitionName], List[Union[str, EnumDefinitionName]]]] = empty_list() diff --git a/linkml_runtime/loaders/rdf_loader.py b/linkml_runtime/loaders/rdf_loader.py index 5a8a2725..4d9887ab 100644 --- a/linkml_runtime/loaders/rdf_loader.py +++ b/linkml_runtime/loaders/rdf_loader.py @@ -18,7 +18,6 @@ class RDFLoader(Loader): def load_any(self, *args, **kwargs) -> Union[YAMLRoot, List[YAMLRoot]]: return self.load(*args, **kwargs) - def load(self, source: Union[str, TextIO, Graph], target_class: Type[YAMLRoot], *, base_dir: Optional[str] = None, contexts: CONTEXTS_PARAM_TYPE = None, fmt: Optional[str] = 'turtle', metadata: Optional[FileInfo] = None) -> YAMLRoot: @@ -52,7 +51,6 @@ def loader(data: Union[str, dict], _: FileInfo) -> Optional[dict]: g.parse(data=data, format=fmt) jsonld_str = g.serialize(format='json-ld', indent=4) data = json.loads(jsonld_str) - #data = pyld_jsonld_from_rdflib_graph(g) if not isinstance(data, dict): # TODO: Add a context processor to the source w/ CONTEXTS_PARAM_TYPE diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py index dfb1916b..7637b5cb 100644 --- a/linkml_runtime/utils/schemaview.py +++ b/linkml_runtime/utils/schemaview.py @@ -5,15 +5,17 @@ from functools import lru_cache from copy import copy, deepcopy from collections import defaultdict, OrderedDict -from typing import Mapping, Tuple, Type +from typing import Mapping, Tuple, Type, Union, Optional, List, Any + +from linkml_runtime.linkml_model import PermissibleValue, PermissibleValueText from linkml_runtime.utils.namespaces import Namespaces from deprecated.classic import deprecated from linkml_runtime.utils.context_utils import parse_import_map, map_import from linkml_runtime.utils.pattern import PatternResolver from linkml_runtime.linkml_model.meta import * from enum import Enum -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) MAPPING_TYPE = str ## e.g. broad, exact, related, ... CACHE_SIZE = 1024 @@ -52,10 +54,10 @@ def _closure(f, x, reflexive=True, depth_first=True, **kwargs): todo = todo[1:] visited.append(i) vals = f(i) - for v in vals: - if v not in visited: - todo.append(v) - if v not in rv: + if vals is not None: + for v in vals: + if v not in visited and v not in rv: + todo.append(v) rv.append(v) return rv @@ -121,7 +123,7 @@ def __init__(self, schema: Union[str, SchemaDefinition], self.uuid = str(uuid.uuid4()) def __key(self): - return (self.schema.id, self.uuid, self.modifications) + return self.schema.id, self.uuid, self.modifications def __eq__(self, other): if isinstance(other, SchemaView): @@ -147,7 +149,8 @@ def load_import(self, imp: str, from_schema: SchemaDefinition = None): sname = map_import(self.importmap, self.namespaces, imp) logging.info(f'Loading schema {sname} from {from_schema.source_file}') schema = load_schema_wrap(sname + '.yaml', - base_dir=os.path.dirname(from_schema.source_file) if from_schema.source_file else None) + base_dir=os.path.dirname( + from_schema.source_file) if from_schema.source_file else None) return schema @lru_cache() @@ -188,7 +191,6 @@ def imports_closure(self, imports: bool = True, traverse=True, inject_metadata=T a.from_schema = s.id return closure - @lru_cache() def all_schema(self, imports: bool = True) -> List[SchemaDefinition]: """ @@ -272,7 +274,8 @@ def all_slot(self, **kwargs) -> Dict[SlotDefinitionName, SlotDefinition]: return self.all_slots(**kwargs) @lru_cache() - def all_slots(self, ordered_by=OrderedBy.PRESERVE, imports=True, attributes=True) -> Dict[SlotDefinitionName, SlotDefinition]: + def all_slots(self, ordered_by=OrderedBy.PRESERVE, imports=True, attributes=True) -> Dict[ + SlotDefinitionName, SlotDefinition]: """ :param ordered_by: an enumerated parameter that returns all the slots in the order specified. :param imports: include imports closure @@ -313,7 +316,6 @@ def all_enums(self, imports=True) -> Dict[EnumDefinitionName, EnumDefinition]: """ return self._get_dict(ENUMS, imports) - @deprecated("Use `all_types` instead") @lru_cache() def all_type(self, imports=True) -> Dict[TypeDefinitionName, TypeDefinition]: @@ -383,7 +385,7 @@ def _get_dict(self, slot_name: str, imports=True) -> Dict: for s in schemas: # get the value of element name from the schema, if empty, return empty dictionary. d1 = getattr(s, slot_name, {}) - # {**d,**d1} syntax merges dictionary a and b into a single dictionary, removing duplicates. + # {**d,**d1} syntax merges dictionary d and d1 into a single dictionary, removing duplicates. d = {**d, **d1} return d @@ -416,7 +418,6 @@ def class_name_mappings(self) -> Dict[str, ClassDefinition]: m[camelcase(s.name)] = s return m - @lru_cache() def in_schema(self, element_name: ElementName) -> SchemaDefinitionName: """ @@ -537,14 +538,32 @@ def class_parents(self, class_name: CLASS_NAME, imports=True, mixins=True, is_a= return self._parents(cls, imports, mixins, is_a) @lru_cache() - def enum_parents(self, enum_name: ENUM_NAME, imports=True, mixins=True, is_a=True) -> List[EnumDefinitionName]: + def enum_parents(self, enum_name: ENUM_NAME, imports=False, mixins=False, is_a=True) -> List[EnumDefinitionName]: """ :param enum_name: child enum name - :param imports: include import closure - :param mixins: include mixins (default is True) + :param imports: include import closure (False) + :param mixins: include mixins (default is False) :return: all direct parent enum names (is_a and mixins) """ - return [] + e = self.get_enum(enum_name, strict=True) + return self._parents(e, imports, mixins, is_a=is_a) + + @lru_cache() + def permissible_value_parent(self, permissible_value: str, enum_name: ENUM_NAME) -> Union[ + str, PermissibleValueText, None, ValueError]: + """ + :param enum_name: child enum name + :param permissible_value: permissible value + :return: all direct parent enum names (is_a) + """ + enum = self.get_enum(enum_name, strict=True) + if enum: + if permissible_value in enum.permissible_values: + pv = enum.permissible_values[permissible_value] + if pv.is_a: + return [pv.is_a] + else: + return [] @lru_cache() def slot_parents(self, slot_name: SLOT_NAME, imports=True, mixins=True, is_a=True) -> List[SlotDefinitionName]: @@ -613,11 +632,26 @@ def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, ref """ return _closure(lambda x: self.class_parents(x, imports=imports, mixins=mixins, is_a=is_a), class_name, - reflexive=reflexive, depth_first=depth_first) + reflexive=reflexive, depth_first=depth_first) + + @lru_cache() + def permissible_value_ancestors(self, permissible_value_text: str, + enum_name: ENUM_NAME, + reflexive=True, + depth_first=True) -> List[str]: + """ + Closure of permissible_value_parents method + :enum + """ + + return _closure(lambda x: self.permissible_value_parent(x, enum_name), + permissible_value_text, + reflexive=reflexive, + depth_first=depth_first) @lru_cache() def enum_ancestors(self, enum_name: ENUM_NAME, imports=True, mixins=True, reflexive=True, is_a=True, - depth_first=True) -> List[EnumDefinitionName]: + depth_first=True) -> List[EnumDefinitionName]: """ Closure of enum_parents method @@ -631,10 +665,11 @@ def enum_ancestors(self, enum_name: ENUM_NAME, imports=True, mixins=True, reflex """ return _closure(lambda x: self.enum_parents(x, imports=imports, mixins=mixins, is_a=is_a), enum_name, - reflexive=reflexive, depth_first=depth_first) + reflexive=reflexive, depth_first=depth_first) @lru_cache() - def type_ancestors(self, type_name: TYPES, imports=True, reflexive=True, depth_first=True) -> List[TypeDefinitionName]: + def type_ancestors(self, type_name: TYPES, imports=True, reflexive=True, depth_first=True) -> List[ + TypeDefinitionName]: """ All ancestors of a type via typeof @@ -646,10 +681,11 @@ def type_ancestors(self, type_name: TYPES, imports=True, reflexive=True, depth_f """ return _closure(lambda x: self.type_parents(x, imports=imports), type_name, - reflexive=reflexive, depth_first=depth_first) + reflexive=reflexive, depth_first=depth_first) @lru_cache() - def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[SlotDefinitionName]: + def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ + SlotDefinitionName]: """ Closure of slot_parents method @@ -665,7 +701,8 @@ def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflex reflexive=reflexive) @lru_cache() - def class_descendants(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ClassDefinitionName]: + def class_descendants(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ + ClassDefinitionName]: """ Closure of class_children method @@ -676,10 +713,12 @@ def class_descendants(self, class_name: CLASS_NAME, imports=True, mixins=True, r :param reflexive: include self in set of descendants :return: descendants class names """ - return _closure(lambda x: self.class_children(x, imports=imports, mixins=mixins, is_a=is_a), class_name, reflexive=reflexive) + return _closure(lambda x: self.class_children(x, imports=imports, mixins=mixins, is_a=is_a), class_name, + reflexive=reflexive) @lru_cache() - def slot_descendants(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[SlotDefinitionName]: + def slot_descendants(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ + SlotDefinitionName]: """ Closure of slot_children method @@ -690,7 +729,8 @@ def slot_descendants(self, slot_name: SLOT_NAME, imports=True, mixins=True, refl :param reflexive: include self in set of descendants :return: descendants slot names """ - return _closure(lambda x: self.slot_children(x, imports=imports, mixins=mixins, is_a=is_a), slot_name, reflexive=reflexive) + return _closure(lambda x: self.slot_children(x, imports=imports, mixins=mixins, is_a=is_a), slot_name, + reflexive=reflexive) @lru_cache() def class_roots(self, imports=True, mixins=True, is_a=True) -> List[ClassDefinitionName]: @@ -718,7 +758,6 @@ def class_leaves(self, imports=True, mixins=True, is_a=True) -> List[ClassDefini for c in self.all_classes(imports=imports) if self.class_children(c, mixins=mixins, is_a=is_a, imports=imports) == []] - @lru_cache() def slot_roots(self, imports=True, mixins=True) -> List[SlotDefinitionName]: """ @@ -881,7 +920,8 @@ def get_elements_applicable_by_prefix(self, prefix: str) -> List[str]: return applicable_elements @lru_cache() - def get_mappings(self, element_name: ElementName = None, imports=True, expand=False) -> Dict[MAPPING_TYPE, List[URIorCURIE]]: + def get_mappings(self, element_name: ElementName = None, imports=True, expand=False) -> Dict[ + MAPPING_TYPE, List[URIorCURIE]]: """ Get all mappings for a given element @@ -1003,9 +1043,9 @@ def annotation_dict(self, element_name: ElementName, imports=True) -> Dict[URIor e = self.get_element(element_name, imports=imports) return {k: v.value for k, v in e.annotations.items()} - @lru_cache() - def class_slots(self, class_name: CLASS_NAME, imports=True, direct=False, attributes=True) -> List[SlotDefinitionName]: + def class_slots(self, class_name: CLASS_NAME, imports=True, direct=False, attributes=True) -> List[ + SlotDefinitionName]: """ :param class_name: :param imports: include imports closure @@ -1030,7 +1070,8 @@ def class_slots(self, class_name: CLASS_NAME, imports=True, direct=False, attrib return slots_nr @lru_cache() - def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, imports=True, mangle_name=False) -> SlotDefinition: + def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, imports=True, + mangle_name=False) -> SlotDefinition: """ Given a slot, in the context of a particular class, yield a dynamic SlotDefinition that has all properties materialized. @@ -1075,7 +1116,6 @@ def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, impo for metaslot_name in SlotDefinition._inherited_slots: if getattr(anc_slot, metaslot_name, None): setattr(induced_slot, metaslot_name, deepcopy(getattr(anc_slot, metaslot_name))) - # Apply slot-usages COMBINE = { 'maximum_value': lambda x, y: min(x, y), 'minimum_value': lambda x, y: max(x, y), @@ -1309,7 +1349,7 @@ def get_classes_by_slot(self, slot: SlotDefinition, include_induced: bool = Fals :param include_induced: supplement all direct slots with induced slots, defaults to False :return: list of slots, either direct, or both direct and induced """ - slots_list = [] # list of all direct or induced slots + slots_list = [] # list of all direct or induced slots for c_name, c in self.all_classes().items(): # check if slot is direct specification on class @@ -1475,7 +1515,7 @@ def delete_subset(self, subset_name: SubsetDefinitionName) -> None: del self.schema.subsetes[subset_name] self.set_modified() - #def rename(self, old_name: str, new_name: str): + # def rename(self, old_name: str, new_name: str): # todo: add to runtime def merge_schema(self, schema: SchemaDefinition, clobber=False) -> None: @@ -1551,7 +1591,7 @@ def materialize_pattern_into_slot_definition(slot_definition: SlotDefinition) -> if class_definition.slot_usage: for slot_definition in class_definition.slot_usage.values(): materialize_pattern_into_slot_definition(slot_definition) - + if class_definition.attributes: for slot_definition in class_definition.attributes.values(): materialize_pattern_into_slot_definition(slot_definition) diff --git a/tests/test_utils/input/kitchen_sink_noimports.yaml b/tests/test_utils/input/kitchen_sink_noimports.yaml index 43a74653..b0326210 100644 --- a/tests/test_utils/input/kitchen_sink_noimports.yaml +++ b/tests/test_utils/input/kitchen_sink_noimports.yaml @@ -354,3 +354,14 @@ enums: permissible_values: a: b: + Animals: + is_a: OtherEnum + permissible_values: + CAT: + LION: + is_a: CAT + ANGRY_LION: + is_a: LION + BIRD: + EAGLE: + is_a: BIRD diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py index 0fba968a..2b76bf77 100644 --- a/tests/test_utils/test_schemaview.py +++ b/tests/test_utils/test_schemaview.py @@ -3,6 +3,7 @@ import logging from copy import copy from typing import List +from unittest import TestCase from linkml_runtime.dumpers import yaml_dumper from linkml_runtime.linkml_model.meta import SchemaDefinition, ClassDefinition, SlotDefinitionName, SlotDefinition, \ @@ -18,58 +19,80 @@ SCHEMA_WITH_STRUCTURED_PATTERNS = os.path.join(INPUT_DIR, "pattern-example.yaml") yaml_loader = YAMLLoader() - +IS_CURRENT = 'is current' +EMPLOYED_AT = 'employed at' +COMPANY = 'Company' +AGENT = 'agent' +ACTIVITY = 'activity' +RELATED_TO = 'related to' +AGE_IN_YEARS = 'age in years' class SchemaViewTestCase(unittest.TestCase): + def test_schemaview_enums(self): + view = SchemaView(SCHEMA_NO_IMPORTS) + for en, e in view.all_enums().items(): + if e.name == "Animals": + for pv, v in e.permissible_values.items(): + if pv == "CAT": + print(view.permissible_value_parent(pv, e.name)) + self.assertEqual(view.permissible_value_parent(pv, e.name), None) + self.assertEqual(view.permissible_value_ancestors(pv, e.name), ['CAT']) + if pv == "ANGRY_LION": + self.assertEqual(view.permissible_value_parent(pv, e.name), ['LION']) + self.assertEqual(view.permissible_value_ancestors(pv, e.name), ['ANGRY_LION', 'LION', 'CAT']) + for cn, c in view.all_classes().items(): + if c.name == "Adult": + self.assertEqual(view.class_ancestors(c.name), ['Adult', 'Person', 'HasAliases', 'Thing']) + def test_schemaview(self): # no import schema view = SchemaView(SCHEMA_NO_IMPORTS) logging.debug(view.imports_closure()) - assert len(view.imports_closure()) == 1 + self.assertEqual(len(view.imports_closure()), 1) all_cls = view.all_classes() logging.debug(f'n_cls = {len(all_cls)}') - assert list(view.annotation_dict('is current').values()) == ['bar'] - logging.debug(view.annotation_dict('employed at')) - e = view.get_element('employed at') + self.assertEqual(list(view.annotation_dict(IS_CURRENT).values()), ['bar']) + logging.debug(view.annotation_dict(EMPLOYED_AT)) + e = view.get_element(EMPLOYED_AT) logging.debug(e.annotations) e = view.get_element('has employment history') logging.debug(e.annotations) elements = view.get_elements_applicable_by_identifier("ORCID:1234") - assert "Person" in elements + self.assertIn("Person", elements) elements = view.get_elements_applicable_by_identifier("PMID:1234") - assert "Organization" in elements + self.assertIn("Organization", elements) elements = view.get_elements_applicable_by_identifier("http://www.ncbi.nlm.nih.gov/pubmed/1234") - assert "Organization" in elements + self.assertIn("Organization", elements) elements = view.get_elements_applicable_by_identifier("TEST:1234") - assert "anatomical entity" not in elements - assert list(view.annotation_dict(SlotDefinitionName('is current')).values()) == ['bar'] - logging.debug(view.annotation_dict(SlotDefinitionName('employed at'))) - element = view.get_element(SlotDefinitionName('employed at')) + self.assertNotIn("anatomical entity", elements) + self.assertEqual(list(view.annotation_dict(SlotDefinitionName(IS_CURRENT)).values()), ['bar']) + logging.debug(view.annotation_dict(SlotDefinitionName(EMPLOYED_AT))) + element = view.get_element(SlotDefinitionName(EMPLOYED_AT)) logging.debug(element.annotations) element = view.get_element(SlotDefinitionName('has employment history')) logging.debug(element.annotations) - assert view.is_mixin('WithLocation') - assert not view.is_mixin('BirthEvent') + self.assertTrue(view.is_mixin('WithLocation')) + self.assertFalse(view.is_mixin('BirthEvent')) - assert view.inverse('employment history of') == 'has employment history' - assert view.inverse('has employment history') == 'employment history of' + self.assertTrue(view.inverse('employment history of'), 'has employment history') + self.assertTrue(view.inverse('has employment history'), 'employment history of') mapping = view.get_mapping_index() - assert mapping is not None + self.assertTrue(mapping is not None) category_mapping = view.get_element_by_mapping("GO:0005198") - assert category_mapping == ['activity'] + self.assertTrue(category_mapping, [ACTIVITY]) - assert view.is_multivalued('aliases') is True - assert view.is_multivalued('id') is False - assert view.is_multivalued('dog addresses') is True + self.assertTrue(view.is_multivalued('aliases')) + self.assertFalse(view.is_multivalued('id')) + self.assertTrue(view.is_multivalued('dog addresses')) - assert view.slot_is_true_for_metadata_property('aliases', 'multivalued') is True - assert view.slot_is_true_for_metadata_property('id', 'identifier') is True + self.assertTrue(view.slot_is_true_for_metadata_property('aliases', 'multivalued')) + self.assertTrue(view.slot_is_true_for_metadata_property('id', 'identifier')) with self.assertRaises(ValueError): view.slot_is_true_for_metadata_property('aliases', 'aliases') @@ -108,98 +131,98 @@ def test_schemaview(self): # -- TEST ANCESTOR/DESCENDANTS FUNCTIONS -- self.assertCountEqual(['Company', 'Organization', 'HasAliases', 'Thing'], - view.class_ancestors('Company')) + view.class_ancestors(COMPANY)) self.assertCountEqual(['Organization', 'HasAliases', 'Thing'], - view.class_ancestors('Company', reflexive=False)) - self.assertCountEqual(['Thing', 'Person', 'Organization', 'Company', 'Adult'], + view.class_ancestors(COMPANY, reflexive=False)) + self.assertCountEqual(['Thing', 'Person', 'Organization', COMPANY, 'Adult'], view.class_descendants('Thing')) # -- TEST CLASS SLOTS -- self.assertCountEqual(['id', 'name', ## From Thing 'has employment history', 'has familial relationships', 'has medical history', - 'age in years', 'addresses', 'has birth event', ## From Person + AGE_IN_YEARS, 'addresses', 'has birth event', ## From Person 'aliases' ## From HasAliases ], view.class_slots('Person')) self.assertCountEqual(view.class_slots('Person'), view.class_slots('Adult')) self.assertCountEqual(['id', 'name', ## From Thing - 'ceo', ## From Company + 'ceo', ## From COMPANY 'aliases' ## From HasAliases ], - view.class_slots('Company')) + view.class_slots(COMPANY)) - assert view.get_class('agent').class_uri == 'prov:Agent' - assert view.get_uri('agent') == 'prov:Agent' - logging.debug(view.get_class('Company').class_uri) + self.assertEqual(view.get_class(AGENT).class_uri, 'prov:Agent') + self.assertEqual(view.get_uri(AGENT), 'prov:Agent') + logging.debug(view.get_class(COMPANY).class_uri) - assert view.get_uri('Company') == 'ks:Company' + self.assertEqual(view.get_uri(COMPANY), 'ks:Company') # test induced slots - for c in ['Company', 'Person', 'Organization',]: + for c in [COMPANY, 'Person', 'Organization',]: islot = view.induced_slot('aliases', c) assert islot.multivalued is True self.assertEqual(islot.owner, c, 'owner does not match') self.assertEqual(view.get_uri(islot, expand=True), 'https://w3id.org/linkml/tests/kitchen_sink/aliases') - assert view.get_identifier_slot('Company').name == 'id' - assert view.get_identifier_slot('Thing').name == 'id' - assert view.get_identifier_slot('FamilialRelationship') is None - for c in ['Company', 'Person', 'Organization', 'Thing']: - assert view.induced_slot('id', c).identifier is True - assert view.induced_slot('name', c).identifier is not True - assert view.induced_slot('name', c).required is False - assert view.induced_slot('name', c).range == 'string' + self.assertEqual(view.get_identifier_slot('Company').name, 'id') + self.assertEqual(view.get_identifier_slot('Thing').name, 'id') + self.assertTrue(view.get_identifier_slot('FamilialRelationship') is None) + for c in [COMPANY, 'Person', 'Organization', 'Thing']: + self.assertTrue(view.induced_slot('id', c).identifier) + self.assertFalse(view.induced_slot('name', c).identifier) + self.assertFalse(view.induced_slot('name', c).required) + self.assertEqual(view.induced_slot('name', c).range, 'string') self.assertEqual(view.induced_slot('id', c).owner, c, 'owner does not match') self.assertEqual(view.induced_slot('name', c).owner, c, 'owner does not match') for c in ['Event', 'EmploymentEvent', 'MedicalEvent']: s = view.induced_slot('started at time', c) logging.debug(f's={s.range} // c = {c}') - assert s.range == 'date' - assert s.slot_uri == 'prov:startedAtTime' + self.assertEqual(s.range, 'date') + self.assertEqual(s.slot_uri, 'prov:startedAtTime') self.assertEqual(s.owner, c, 'owner does not match') c_induced = view.induced_class(c) # an induced class should have no slots - assert c_induced.slots == [] - assert c_induced.attributes != [] + self.assertEqual(c_induced.slots, []) + self.assertNotEqual(c_induced.attributes, []) s2 = c_induced.attributes['started at time'] - assert s2.range == 'date' - assert s2.slot_uri == 'prov:startedAtTime' + self.assertEqual(s2.range, 'date') + self.assertEqual(s2.slot_uri, 'prov:startedAtTime') # test slot_usage - assert view.induced_slot('age in years', 'Person').minimum_value == 0 - assert view.induced_slot('age in years', 'Adult').minimum_value == 16 - assert view.induced_slot('name', 'Person').pattern is not None - assert view.induced_slot('type', 'FamilialRelationship').range == 'FamilialRelationshipType' - assert view.induced_slot('related to', 'FamilialRelationship').range == 'Person' - assert view.get_slot('related to').range == 'Thing' - assert view.induced_slot('related to', 'Relationship').range == 'Thing' + self.assertEqual(view.induced_slot(AGE_IN_YEARS, 'Person').minimum_value, 0) + self.assertEqual(view.induced_slot(AGE_IN_YEARS, 'Adult').minimum_value, 16) + self.assertTrue(view.induced_slot('name', 'Person').pattern is not None) + self.assertEqual(view.induced_slot('type', 'FamilialRelationship').range, 'FamilialRelationshipType') + self.assertEqual(view.induced_slot(RELATED_TO, 'FamilialRelationship').range, 'Person') + self.assertEqual(view.get_slot(RELATED_TO).range, 'Thing') + self.assertEqual(view.induced_slot(RELATED_TO, 'Relationship').range, 'Thing') # https://github.com/linkml/linkml/issues/875 self.assertCountEqual(['Thing', 'Place'], view.induced_slot('name').domain_of) - a = view.get_class('activity') + a = view.get_class(ACTIVITY) self.assertCountEqual(a.exact_mappings, ['prov:Activity']) - logging.debug(view.get_mappings('activity', expand=True)) - self.assertCountEqual(view.get_mappings('activity')['exact'], ['prov:Activity']) - self.assertCountEqual(view.get_mappings('activity', expand=True)['exact'], ['http://www.w3.org/ns/prov#Activity']) + logging.debug(view.get_mappings(ACTIVITY, expand=True)) + self.assertCountEqual(view.get_mappings(ACTIVITY)['exact'], ['prov:Activity']) + self.assertCountEqual(view.get_mappings(ACTIVITY, expand=True)['exact'], ['http://www.w3.org/ns/prov#Activity']) u = view.usage_index() for k, v in u.items(): #print(f' {k} = {v}') logging.debug(f' {k} = {v}') - assert SchemaUsage(used_by='FamilialRelationship', slot='related to', - metaslot='range', used='Person', inferred=False) in u['Person'] + self.assertIn(SchemaUsage(used_by='FamilialRelationship', slot=RELATED_TO, + metaslot='range', used='Person', inferred=False), u['Person']) # test methods also work for attributes leaves = view.class_leaves() logging.debug(f'LEAVES={leaves}') - assert 'MedicalEvent' in leaves + self.assertIn('MedicalEvent', leaves) roots = view.class_roots() logging.debug(f'ROOTS={roots}') - assert 'Dataset' in roots + self.assertIn('Dataset', roots) ds_slots = view.class_slots('Dataset') logging.debug(ds_slots) - assert len(ds_slots) == 3 + self.assertEquals(len(ds_slots), 3) self.assertCountEqual(['persons', 'companies', 'activities'], ds_slots) for sn in ds_slots: s = view.induced_slot(sn, 'Dataset') @@ -212,7 +235,7 @@ def test_all_classes_ordered_lexical(self): ordered_c = [] for c in classes.values(): ordered_c.append(c.name) - assert ordered_c == sorted(ordered_c) + self.assertEquals(ordered_c, sorted(ordered_c)) def test_all_classes_ordered_rank(self): view = SchemaView(SCHEMA_NO_IMPORTS) @@ -227,8 +250,8 @@ def test_all_classes_ordered_rank(self): first_in_line.append(name) elif definition.rank == 2: second_in_line.append(name) - assert ordered_c[0] in first_in_line - assert ordered_c[10] not in second_in_line + self.assertIn(ordered_c[0], first_in_line) + self.assertNotIn(ordered_c[10], second_in_line) def test_all_classes_ordered_no_ordered_by(self): view = SchemaView(SCHEMA_NO_IMPORTS) @@ -247,7 +270,7 @@ def test_all_slots_ordered_lexical(self): for s in slots.values(): ordered_s.append(s.name) print(ordered_s) - assert ordered_s == sorted(ordered_s) + self.assertEqual(ordered_s, sorted(ordered_s)) def test_all_slots_ordered_rank(self): view = SchemaView(SCHEMA_NO_IMPORTS) @@ -263,9 +286,8 @@ def test_all_slots_ordered_rank(self): first_in_line.append(name) elif definition.rank == 2: second_in_line.append(name) - assert ordered_s[0] in first_in_line - assert ordered_s[10] not in second_in_line - + self.assertIn(ordered_s[0], first_in_line) + self.assertNotIn(ordered_s[10], second_in_line) def test_rollup_rolldown(self): # no import schema @@ -276,10 +298,10 @@ def test_rollup_rolldown(self): logging.debug(slot) induced_slot_names = [s.name for s in view.class_induced_slots(element_name)] logging.debug(induced_slot_names) - self.assertCountEqual(['started at time', 'ended at time', 'is current', 'in location', 'employed at', 'married to'], + self.assertCountEqual(['started at time', 'ended at time', IS_CURRENT, 'in location', EMPLOYED_AT, 'married to'], induced_slot_names) # check to make sure rolled-up classes are deleted - assert view.class_descendants(element_name, reflexive=False) == [] + self.assertEqual(view.class_descendants(element_name, reflexive=False), []) roll_down(view, view.class_leaves()) for element_name in view.all_classes(): @@ -288,9 +310,9 @@ def test_rollup_rolldown(self): logging.debug(f' {element_name} SLOTS(i) = {view.class_slots(element_name)}') logging.debug(f' {element_name} SLOTS(d) = {view.class_slots(element_name, direct=True)}') self.assertCountEqual(view.class_slots(element_name), view.class_slots(element_name, direct=True)) - assert 'Thing' not in view.all_classes() - assert 'Person' not in view.all_classes() - assert 'Adult' in view.all_classes() + self.assertNotIn('Thing', view.all_classes()) + self.assertNotIn('Person', view.all_classes()) + self.assertIn('Adult', view.all_classes()) def test_caching(self): """ @@ -329,15 +351,15 @@ def test_imports(self): self.assertCountEqual(['kitchen_sink', 'core', 'linkml:types'], view.imports_closure()) for t in view.all_types().keys(): logging.debug(f'T={t} in={view.in_schema(t)}') - assert view.in_schema(ClassDefinitionName('Person')) == 'kitchen_sink' - assert view.in_schema(SlotDefinitionName('id')) == 'core' - assert view.in_schema(SlotDefinitionName('name')) == 'core' - assert view.in_schema(SlotDefinitionName('activity')) == 'core' - assert view.in_schema(SlotDefinitionName('string')) == 'types' - assert 'activity' in view.all_classes() - assert 'activity' not in view.all_classes(imports=False) - assert 'string' in view.all_types() - assert 'string' not in view.all_types(imports=False) + self.assertEquals(view.in_schema(ClassDefinitionName('Person')), 'kitchen_sink') + self.assertEquals(view.in_schema(SlotDefinitionName('id')), 'core') + self.assertEquals(view.in_schema(SlotDefinitionName('name')), 'core') + self.assertEquals(view.in_schema(SlotDefinitionName(ACTIVITY)), 'core') + self.assertEquals(view.in_schema(SlotDefinitionName('string')), 'types') + self.assertIn(ACTIVITY, view.all_classes()) + self.assertNotIn(ACTIVITY, view.all_classes(imports=False)) + self.assertIn('string', view.all_types()) + self.assertNotIn('string', view.all_types(imports=False)) self.assertCountEqual(['SymbolString', 'string'], view.type_ancestors('SymbolString')) for tn, t in view.all_types().items(): @@ -356,20 +378,16 @@ def test_imports(self): self.assertEqual('https://w3id.org/linkml/tests/kitchen_sink', e.from_schema) else: self.assertEqual('https://w3id.org/linkml/tests/core', e.from_schema) - #for pv in e.permissible_values.values(): - # print(f'{pv.text}: {pv.from_schema} : {view.slot_permissible_value_ancestors(pv)}') for sn, s in view.all_slots().items(): self.assertEqual(sn, s.name) s_induced = view.induced_slot(sn) self.assertIsNotNone(s_induced.range) - #self.assertIsNotNone(s_induced.slot_uri) if s in view.all_slots(imports=False).values(): self.assertEqual('https://w3id.org/linkml/tests/kitchen_sink', s.from_schema) else: self.assertEqual('https://w3id.org/linkml/tests/core', s.from_schema) for cn, c in view.all_classes().items(): self.assertEqual(cn, c.name) - #self.assertIsNotNone(c.class_uri) if c in view.all_classes(imports=False).values(): self.assertEqual('https://w3id.org/linkml/tests/kitchen_sink', c.from_schema) else: @@ -378,34 +396,30 @@ def test_imports(self): if s in view.all_classes(imports=False).values(): self.assertIsNotNone(s.slot_uri) self.assertEqual('https://w3id.org/linkml/tests/kitchen_sink', s.from_schema) - #else: - # self.assertEqual('https://w3id.org/linkml/tests/core', s.from_schema) - for c in ['Company', 'Person', 'Organization', 'Thing']: - assert view.induced_slot('id', c).identifier is True - assert view.induced_slot('name', c).identifier is not True - assert view.induced_slot('name', c).required is False - assert view.induced_slot('name', c).range == 'string' + self.assertTrue(view.induced_slot('id', c).identifier) + self.assertFalse(view.induced_slot('name', c).identifier) + self.assertFalse(view.induced_slot('name', c).required) + self.assertEquals(view.induced_slot('name', c).range, 'string') for c in ['Event', 'EmploymentEvent', 'MedicalEvent']: s = view.induced_slot('started at time', c) - #print(f's={s.range} // c = {c}') - assert s.range == 'date' - assert s.slot_uri == 'prov:startedAtTime' - assert view.induced_slot('age in years', 'Person').minimum_value == 0 - assert view.induced_slot('age in years', 'Adult').minimum_value == 16 - - assert view.get_class('agent').class_uri == 'prov:Agent' - assert view.get_uri('agent') == 'prov:Agent' + self.assertEquals(s.range, 'date') + self.assertEquals(s.slot_uri, 'prov:startedAtTime') + self.assertEquals(view.induced_slot(AGE_IN_YEARS, 'Person').minimum_value, 0) + self.assertEquals(view.induced_slot(AGE_IN_YEARS, 'Adult').minimum_value, 16) + + self.assertEquals(view.get_class('agent').class_uri, 'prov:Agent') + self.assertEquals(view.get_uri(AGENT), 'prov:Agent') logging.debug(view.get_class('Company').class_uri) - assert view.get_uri('Company') == 'ks:Company' - assert view.get_uri('Company', expand=True) == 'https://w3id.org/linkml/tests/kitchen_sink/Company' - logging.debug(view.get_uri("TestClass")) - assert view.get_uri('TestClass') == 'core:TestClass' - assert view.get_uri('TestClass', expand=True) == 'https://w3id.org/linkml/tests/core/TestClass' + self.assertEquals(view.get_uri(COMPANY), 'ks:Company') + self.assertEquals(view.get_uri(COMPANY, expand=True), 'https://w3id.org/linkml/tests/kitchen_sink/Company') + logging.debug(view.get_uri('TestClass')) + self.assertEquals(view.get_uri('TestClass'), 'core:TestClass') + self.assertEquals(view.get_uri('TestClass', expand=True), 'https://w3id.org/linkml/tests/core/TestClass') - assert view.get_uri('string') == 'xsd:string' + self.assertEquals(view.get_uri('string'), 'xsd:string') # dynamic enums e = view.get_enum('HCAExample') @@ -424,12 +438,12 @@ def test_merge_imports(self): view = SchemaView(SCHEMA_WITH_IMPORTS) all_c = copy(view.all_classes()) all_c_noi = copy(view.all_classes(imports=False)) - assert len(all_c_noi) < len(all_c) + self.assertLess(len(all_c_noi), len(all_c)) view.merge_imports() all_c2 = copy(view.all_classes()) self.assertCountEqual(all_c, all_c2) all_c2_noi = copy(view.all_classes(imports=False)) - assert len(all_c2_noi) == len(all_c2) + self.assertEqual(len(all_c2_noi), len(all_c2)) def test_traversal(self): schema = SchemaDefinition(id='test', name='traversal-test') @@ -448,8 +462,8 @@ def test_traversal(self): view.add_class(ClassDefinition('AZ', is_a='RootMixin', mixin=True)) view.add_class(ClassDefinition('BY', is_a='RootMixin', mixin=True)) view.add_class(ClassDefinition('CX', is_a='RootMixin', mixin=True)) + def check(ancs: List, expected: List): - #print(ancs) self.assertEqual(ancs, expected) check(view.class_ancestors('C', depth_first=True), ['C', 'Cm1', 'Cm2', 'CX', 'B', 'Bm1', 'Bm2', 'BY', 'A', 'Am1', 'Am2', 'AZ', 'Root', 'RootMixin']) @@ -476,18 +490,18 @@ def test_slot_inheritance(self): view.add_slot(SlotDefinition('s4', is_a='s2', mixins=['m1'], range='W')) view.add_slot(SlotDefinition('m1', mixin=True, multivalued=False, range='Z')) slot1 = view.induced_slot('s1', 'C') - assert not slot1.is_a + self.assertEquals(slot1.is_a, None) self.assertEqual('D', slot1.range) - assert slot1.multivalued + self.assertIsNotNone(slot1.multivalued) slot2 = view.induced_slot('s2', 'C') self.assertEqual(slot2.is_a, 's1') self.assertEqual('D', slot2.range) - assert slot2.multivalued + self.assertIsNotNone(slot2.multivalued) slot3 = view.induced_slot('s3', 'C') - assert slot3.multivalued + self.assertIsNotNone(slot3.multivalued) self.assertEqual('Z', slot3.range) slot4 = view.induced_slot('s4', 'C') - assert slot4.multivalued + self.assertIsNotNone(slot4.multivalued) self.assertEqual('W', slot4.range) # test dangling view.add_slot(SlotDefinition('s5', is_a='does-not-exist')) @@ -551,22 +565,21 @@ def test_ambiguous_attributes(self): self.assertEqual(a1x.range, view.induced_slot(a1x.name, 'C2').range) self.assertEqual(a2x.range, view.induced_slot(a2x.name, 'C2').range) - def test_metamodel_in_schemaview(self): view = package_schemaview('linkml_runtime.linkml_model.meta') for cn in ['class_definition', 'type_definition', 'slot_definition']: - assert cn in view.all_classes() - assert cn in view.all_classes(imports=False) - assert view.get_identifier_slot(cn).name == 'name' + self.assertIn(cn, view.all_classes()) + self.assertIn(cn, view.all_classes(imports=False)) + self.assertEqual(view.get_identifier_slot(cn).name, 'name') for cn in ['annotation', 'extension']: - assert cn in view.all_classes() - assert cn not in view.all_classes(imports=False) + self.assertIn(cn, view.all_classes()) + self.assertNotIn(cn, view.all_classes(imports=False)) for sn in ['id', 'name', 'description']: - assert sn in view.all_slots() + self.assertIn(sn, view.all_slots()) for tn in ['uriorcurie', 'string', 'float']: - assert tn in view.all_types() + self.assertIn(tn, view.all_types()) for tn in ['uriorcurie', 'string', 'float']: - assert tn not in view.all_types(imports=False) + self.assertNotIn(tn, view.all_types(imports=False)) for cn, c in view.all_classes().items(): uri = view.get_uri(cn, expand=True) #print(f'{cn}: {c.class_uri} // {uri}') @@ -583,8 +596,7 @@ def test_metamodel_in_schemaview(self): def test_get_classes_by_slot(self): sv = SchemaView(SCHEMA_WITH_IMPORTS) - TEST_SLOT = "age in years" - slot = sv.get_slot(TEST_SLOT) + slot = sv.get_slot(AGE_IN_YEARS) actual_result = sv.get_classes_by_slot(slot) expected_result = ["Person"] @@ -668,5 +680,6 @@ def test_mergeimports(self): prefixes_list ) + if __name__ == '__main__': unittest.main()