Source code for linkml.validator.plugins.instantiates_validation_plugin
"""Validation plugin that enforces constraints from ``instantiates`` annotations.
When a class declares ``instantiates: [SomeClass]``, and ``SomeClass`` carries
annotations such as ``must_not_have_id_slot: true`` or ``must_be_inlined: true``,
this plugin verifies that the instantiating class actually satisfies those
constraints.
Supported annotations on the instantiated class:
* ``must_not_have_id_slot`` – the instantiating class must **not** define an
identifier slot.
* ``must_be_inlined`` – every slot in the schema whose range is the
instantiating class must have ``inlined: true`` (or ``inlined_as_list: true``).
"""
from __future__ import annotations
from collections.abc import Iterator
from linkml.validator.plugins.validation_plugin import ValidationPlugin
from linkml.validator.report import Severity, ValidationResult
from linkml.validator.validation_context import ValidationContext
from linkml_runtime.utils.schemaview import SchemaView
def _check_must_not_have_id_slot(
class_name: str,
instantiated_name: str,
schema_view: SchemaView,
) -> Iterator[ValidationResult]:
"""Yield a result if *class_name* has an identifier slot."""
id_slot = schema_view.get_identifier_slot(class_name)
if id_slot is not None:
yield ValidationResult(
type="instantiates",
severity=Severity.ERROR,
message=(
f"Class '{class_name}' instantiates '{instantiated_name}' which "
f"requires no identifier slot, but '{class_name}' has identifier "
f"slot '{id_slot.name}'"
),
)
def _check_must_be_inlined(
class_name: str,
instantiated_name: str,
schema_view: SchemaView,
) -> Iterator[ValidationResult]:
"""Yield a result for each slot whose range is *class_name* but is not inlined."""
for owner_class_name in schema_view.all_classes():
for slot in schema_view.class_induced_slots(owner_class_name):
if slot.range != class_name:
continue
if not (slot.inlined or slot.inlined_as_list):
yield ValidationResult(
type="instantiates",
severity=Severity.ERROR,
message=(
f"Class '{class_name}' instantiates '{instantiated_name}' which "
f"requires inlined usage, but slot '{slot.name}' on class "
f"'{owner_class_name}' has range '{class_name}' without "
f"inlined=true"
),
)
# Maps annotation tag → checker function
_ANNOTATION_CHECKERS = {
"must_not_have_id_slot": _check_must_not_have_id_slot,
"must_be_inlined": _check_must_be_inlined,
}
def check_instantiates_constraints(schema_view: SchemaView) -> Iterator[ValidationResult]:
"""Check all ``instantiates`` constraints across the schema.
For every class that has ``instantiates``, look up each instantiated class,
read its annotations, and delegate to the appropriate checker.
:param schema_view: A :class:`SchemaView` over the schema to check.
:return: An iterator of :class:`ValidationResult` for any violations found.
"""
for class_name, class_def in schema_view.all_classes().items():
if not class_def.instantiates:
continue
for instantiated_uri in class_def.instantiates:
instantiated_class = schema_view.get_class(str(instantiated_uri))
if instantiated_class is None:
yield ValidationResult(
type="instantiates",
severity=Severity.WARN,
message=(
f"Class '{class_name}' instantiates '{instantiated_uri}' "
f"which could not be resolved in the schema"
),
)
continue
annotations = instantiated_class.annotations or {}
for annotation_tag, checker_fn in _ANNOTATION_CHECKERS.items():
annotation = annotations.get(annotation_tag)
if annotation is None:
continue
# Annotation value may be an Annotation object or a raw value
value = getattr(annotation, "value", annotation)
if str(value).lower() in ("true", "1", "yes"):
yield from checker_fn(class_name, str(instantiated_uri), schema_view)
[docs]
class InstantiatesValidationPlugin(ValidationPlugin):
"""Validation plugin that checks ``instantiates`` annotation constraints.
This plugin performs schema-level checks: it verifies that classes using
``instantiates`` comply with annotations on the instantiated classes.
Results are computed once and yielded on the first ``process()`` call.
"""
def __init__(self) -> None:
self._results: list[ValidationResult] | None = None
def pre_process(self, context: ValidationContext) -> None:
"""Compute and cache schema-level validation results."""
self._results = list(check_instantiates_constraints(context.schema_view))
def process(self, instance: dict, context: ValidationContext) -> Iterator[ValidationResult]:
"""Yield any cached schema-level results, then clear them.
Since instantiates constraints are schema-level (not per-instance),
results are yielded only once, on the first call to ``process()``.
"""
if self._results:
yield from self._results
self._results = []