Skip to content
Draft
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
29 changes: 28 additions & 1 deletion src/sssom/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import uuid
from enum import Enum
from functools import cached_property, lru_cache
from typing import Any, Dict, List, Literal, Set, TextIO, Union
from typing import Any, Dict, List, Literal, Optional, Set, TextIO, Tuple, Union

import importlib_resources
import yaml
Expand Down Expand Up @@ -283,6 +283,33 @@ def propagatable_slots(self) -> List[str]:
slots.append(slot_name)
return slots

def get_minimum_version(
self, slot_name: str, class_name: str = "mapping"
) -> Optional[Tuple[int, int]]:
"""Get the minimum version of SSSOM required for a given slot.

:param slot_name: The queried slot.
:param class_name: The class the slot belongs to. This is needed
because a slot may have been added to a class
in a later version than the version in which
it was first introduced in the schema.
:return: A tuple containing the major and minor numbers of the
earliest version of SSSOM that defines the given slot
in the given class. May be None if the requested slot
name is not a valid slot name.
"""
try:
slot = self.view.induced_slot(slot_name, class_name)
version = [int(s) for s in slot.annotations.added_in.value.split(".")]
if len(version) != 2:
# Should never happen, schema is incorrect
return None
return (version[0], version[1])
except AttributeError: # No added_in annotation, defaults to 1.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there no other theoretical scenarions the "AttributeError" is thrown in the try block?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No realistic one. AttributeError is thrown when trying to access an attribute that doesn’t exist.

Which attribute are we trying to access here?

  • self.view: this one has to exist, it is defined a few lines above in the same file.
  • self.view.induced_slot: This field is defined in LinkML’s SchemaView class, of which self.view is an instance.
  • slot.annotations: This field is defined in LinkML’s SlotDefinition class, of which slot (as returned by the function above) should be an instance – if it is not, then there must be a pretty serious bug in LinkML.
  • slot.annotations.added_in: This is the field that may not exist, if the slot we are looking at is not tagged with a added_in annotation in the LinkML model.
  • slot.annotations.added_in.value: If the added_in field exists, then it should always have this field – again, if it does not, then something is wrong with LinkML.

return (1, 0)
except ValueError: # No such slot
return None


@lru_cache(1)
def _get_sssom_schema_object() -> SSSOMSchemaView:
Expand Down
43 changes: 43 additions & 0 deletions src/sssom/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
OBJECT_ID,
OBJECT_LABEL,
OBJECT_SOURCE,
OBJECT_TYPE,
OBO_HAS_DB_XREF,
OWL_DIFFERENT_FROM,
OWL_EQUIVALENT_CLASS,
Expand All @@ -58,6 +59,7 @@
SUBJECT_ID,
SUBJECT_LABEL,
SUBJECT_SOURCE,
SUBJECT_TYPE,
UNKNOWN_IRI,
MetadataType,
PathOrIO,
Expand Down Expand Up @@ -496,6 +498,47 @@ def _to_string(row: dict[str, Any], side: str) -> str:
# No scope, so remove any pre-existing "cardinality_scope" column
self.df.drop(columns=CARDINALITY_SCOPE, inplace=True, errors="ignore")

def get_compatible_version(self) -> str:
"""Get the minimum version of SSSOM this set is compatible with."""
schema = SSSOMSchemaView()
versions: Set[Tuple[int, int]] = set()

# First get the minimum versions required by the slots present
# in the set; this is entirely provided by the SSSOM model.
for slot in self.metadata.keys():
version = schema.get_minimum_version(slot, "mapping set")
if version is not None:
versions.add(version)
for slot in self.df.columns:
version = schema.get_minimum_version(slot, "mapping")
if version is not None:
versions.add(version)

# Then take care of enum values; we cannot use the SSSOM model
# for that (enum values are not tagged with an "added_in"
# annotation the way slots are), so this has to be handled
# "manually" based on the informations provided in
# <https://mapping-commons.github.io/sssom/spec-model/#model-changes-across-versions>.
if (
self.metadata.get(SUBJECT_TYPE) == "composed entity expression"
or self.metadata.get(OBJECT_TYPE) == "composed entity expression"
or (
SUBJECT_TYPE in self.df.columns
and "composed entity expression" in self.df[SUBJECT_TYPE].values
)
or (
OBJECT_TYPE in self.df.columns
and "composed entity expression" in self.df[OBJECT_TYPE].values
)
):
versions.add((1, 1))

if MAPPING_CARDINALITY in self.df.columns and "0:0" in self.df[MAPPING_CARDINALITY].values:
versions.add((1, 1))

# Get the highest of the accumulated versions.
return ".".join([str(i) for i in max(versions)])


def _standardize_curie_or_iri(curie_or_iri: str, *, converter: Converter) -> str:
"""Standardize a CURIE or IRI, returning the original if not possible.
Expand Down
40 changes: 40 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
MAPPING_CARDINALITY,
OBJECT_ID,
OBJECT_LABEL,
OBJECT_TYPE,
PREDICATE_ID,
PREDICATE_TYPE,
SEMAPV,
SUBJECT_ID,
SUBJECT_LABEL,
SUBJECT_TYPE,
)
from sssom.context import SSSOM_BUILT_IN_PREFIXES, ensure_converter
from sssom.io import extract_iris
Expand Down Expand Up @@ -634,3 +637,40 @@ def test_infer_scoped_cardinality(self) -> None:
expected = ["1:n", "1:n", "1:n", "1:n", "1:n", "1:n"]
self.assertEqual(expected, list(msdf.df[MAPPING_CARDINALITY].values))
self.assertNotIn(CARDINALITY_SCOPE, msdf.df.columns)

def test_inferring_compatible_version(self) -> None:
"""Test that we can correctly infer the version a set is compatible with."""
msdf10 = parse_sssom_table(f"{data_dir}/basic.tsv")

# Nothing in that set requires 1.1
self.assertEqual("1.0", msdf10.get_compatible_version())

def _clone(msdf):
return MappingSetDataFrame(df=msdf.df.copy(), metadata=msdf.metadata.copy())

# Inject a 1.1-specific mapping set slot
msdf11 = _clone(msdf10)
msdf11.metadata[CARDINALITY_SCOPE] = "predicate_id"
self.assertEqual("1.1", msdf11.get_compatible_version())

# Inject a 1.1-specific mapping slot
msdf11 = _clone(msdf10)
msdf11.df[PREDICATE_TYPE] = "owl object property"
self.assertEqual("1.1", msdf11.get_compatible_version())

# Inject a 1.1-specific entity_type_enum value
msdf11 = _clone(msdf10)
msdf11.metadata[SUBJECT_TYPE] = "composed entity expression"
self.assertEqual("1.1", msdf11.get_compatible_version())

# Same, but on a single mapping record
msdf11 = _clone(msdf10)
msdf11.df[OBJECT_TYPE] = "owl class"
msdf11.df.loc[2, OBJECT_TYPE] = "composed entity expression"
self.assertEqual("1.1", msdf11.get_compatible_version())

# Inject the 1.1-specific "0:0" cardinality value
msdf11 = _clone(msdf10)
msdf11.df[MAPPING_CARDINALITY] = "1:1"
msdf11.df.loc[9, MAPPING_CARDINALITY] = "0:0"
self.assertEqual("1.1", msdf11.get_compatible_version())
Loading