"""Validates of Def, Def-expand and Temporal groups."""
from __future__ import annotations
from hed.errors.error_reporter import ErrorHandler
from hed.errors.error_types import TemporalErrors, ValidationErrors
from hed.models.definition_dict import DefinitionDict
from hed.models.hed_group import HedGroup
from hed.models.hed_tag import HedTag
from hed.models.model_constants import DefTagNames
[docs]
class DefValidator(DefinitionDict):
"""Validates Def/ and Def-expand/, as well as Temporal groups: Onset, Inset, and Offset"""
[docs]
def __init__(self, def_dicts=None, hed_schema=None):
"""Initialize for definitions in HED strings.
Parameters:
def_dicts (list or DefinitionDict or str): DefinitionDicts containing the definitions to pass to baseclass
hed_schema(HedSchema or None): Required if passing strings or lists of strings, unused otherwise.
"""
super().__init__(def_dicts, hed_schema=hed_schema)
@staticmethod
def _report_missing_or_invalid_value(def_tag, def_entry, is_def_expand_tag) -> list[dict]:
"""Returns the correct error for this type of def tag
Parameters:
def_tag(HedTag): The source tag
def_entry(DefinitionEntry): The entry for this definition
is_def_expand_tag(bool): If the given def_tag is a def-expand tag or not.
Returns:
list[dict]: Issues found from validating placeholders.
"""
def_issues = []
if def_entry.takes_value:
error_code = ValidationErrors.HED_DEF_VALUE_MISSING
if is_def_expand_tag:
error_code = ValidationErrors.HED_DEF_EXPAND_VALUE_MISSING
else:
error_code = ValidationErrors.HED_DEF_VALUE_EXTRA
if is_def_expand_tag:
error_code = ValidationErrors.HED_DEF_EXPAND_VALUE_EXTRA
def_issues += ErrorHandler.format_error(error_code, tag=def_tag)
return def_issues
def _validate_def_contents(self, def_tag, def_expand_group) -> list[dict]:
"""Check for issues with expanding a tag from Def to a Def-expand tag group
Parameters:
def_tag (HedTag): Source HED tag that may be a Def or Def-expand tag.
def_expand_group (HedGroup or HedTag): Source group for this def-expand tag.
Same as def_tag if this is not a def-expand tag.
Returns:
list[dict]: Issues found from validating placeholders.
"""
is_def_expand_tag = def_expand_group != def_tag
tag_label, _, placeholder = def_tag.extension.partition("/")
label_tag_lower = tag_label.casefold()
# Check if def_entry in def_dicts.
def_entry = self.defs.get(label_tag_lower)
if def_entry is None or def_entry.takes_value == (not placeholder):
error_code = ValidationErrors.HED_DEF_UNMATCHED
if is_def_expand_tag:
error_code = ValidationErrors.HED_DEF_EXPAND_UNMATCHED
return ErrorHandler.format_error(error_code, tag=def_tag)
# Check the special case of a definition without contents.
def_contents = def_entry.get_definition(def_tag, placeholder_value=placeholder, return_copy_of_tag=True)
if is_def_expand_tag and self._order_independent_form(def_expand_group) != self._order_independent_form(
def_contents
):
return ErrorHandler.format_error(
ValidationErrors.HED_DEF_EXPAND_INVALID,
tag=def_tag,
actual_def=def_contents,
found_def=def_expand_group,
)
return []
@staticmethod
def _order_independent_form(group) -> tuple:
"""Return the tags and subgroups of a group as nested sorted tuples.
Two groups have the same form exactly when they contain the same tags (by short form with
any value or extension, ignoring case as HedTag equality does) and the same subgroups, in any
order. HedGroup equality is order-sensitive, so this form is what a Def-expand group is
compared against its definition with.
Parameters:
group (HedGroup): The group to convert.
Returns:
tuple: (sorted tuple of tag short forms, sorted tuple of subgroup forms).
"""
tags = sorted(tag.short_tag.casefold() for tag in group.tags())
groups = sorted(DefValidator._order_independent_form(subgroup) for subgroup in group.groups())
return tuple(tags), tuple(groups)
[docs]
def validate_def_value_units(self, def_tag, hed_validator, allow_placeholders=False) -> list[dict]:
"""Equivalent to HedValidator.validate_units for the special case of a Def or Def-expand tag"""
tag_label, _, placeholder = def_tag.extension.partition("/")
is_def_expand_tag = def_tag.short_base_tag == DefTagNames.DEF_EXPAND_KEY
def_entry = self.defs.get(tag_label.casefold())
# These errors will be caught as can't match definition
if def_entry is None:
return []
# Make sure that there aren't any errant placeholders.
if not allow_placeholders and "#" in placeholder:
return ErrorHandler.format_error(ValidationErrors.HED_PLACEHOLDER_OUT_OF_CONTEXT, tag=def_tag.tag)
# Set the appropriate error code
error_code = ValidationErrors.DEF_INVALID
if is_def_expand_tag:
error_code = ValidationErrors.DEF_EXPAND_INVALID
# Validate the def name vs the name class
def_issues = hed_validator._unit_validator._check_value_class(def_tag, tag_label, report_as=None)
def_contents = def_entry.get_definition(def_tag, placeholder_value=placeholder, return_copy_of_tag=True)
if def_contents and def_entry.takes_value and hed_validator:
placeholder_tag = def_contents.get_first_group().find_placeholder_tag()
def_issues += hed_validator.validate_units(
placeholder_tag, placeholder, report_as=def_tag, error_code=error_code, index_offset=len(tag_label) + 1
)
return def_issues
[docs]
def validate_onset_offset(self, hed_string_obj) -> list[dict]:
"""Validate onset/offset
Parameters:
hed_string_obj (HedString): The HED string to check.
Returns:
list[dict]: A list of issues found in validating onsets (i.e., out of order onsets, unknown def names).
"""
onset_issues = []
for found_onset, found_group in self._find_onset_tags(hed_string_obj):
if not found_onset:
return []
def_tags = found_group.find_def_tags()
if not def_tags:
onset_issues += ErrorHandler.format_error(TemporalErrors.ONSET_NO_DEF_TAG_FOUND, found_onset)
continue
if len(def_tags) > 1:
onset_issues += ErrorHandler.format_error(
TemporalErrors.ONSET_TOO_MANY_DEFS, tag=def_tags[0][0], tag_list=[tag[0] for tag in def_tags[1:]]
)
continue
# Get all children but def group and onset/offset, then validate #/type of children.
def_tag, def_group, _ = def_tags[0]
if def_group is None:
def_group = def_tag
children = [child for child in found_group.children if def_group is not child and found_onset is not child]
# Delay tag is checked for uniqueness elsewhere, so we can safely remove all of them
children = [
child
for child in children
if not isinstance(child, HedTag) or child.short_base_tag != DefTagNames.DELAY_KEY
]
max_children = 1
if found_onset.short_base_tag == DefTagNames.OFFSET_KEY:
max_children = 0
if len(children) > max_children:
onset_issues += ErrorHandler.format_error(
TemporalErrors.ONSET_WRONG_NUMBER_GROUPS, def_tag, found_group.children
)
continue
if children:
# Make this a loop if max_children can be > 1
child = children[0]
if not isinstance(child, HedGroup):
onset_issues += ErrorHandler.format_error(TemporalErrors.ONSET_TAG_OUTSIDE_OF_GROUP, child, def_tag)
# At this point we have either an onset or offset tag and it's name
onset_issues += self._handle_onset_or_offset(def_tag)
return onset_issues
@staticmethod
def _find_onset_tags(hed_string_obj):
return hed_string_obj.find_top_level_tags(anchor_tags=DefTagNames.TEMPORAL_KEYS)
def _handle_onset_or_offset(self, def_tag) -> list[dict]:
def_name, _, placeholder = def_tag.extension.partition("/")
def_entry = self.defs.get(def_name.casefold())
if def_entry is None:
return ErrorHandler.format_error(TemporalErrors.ONSET_DEF_UNMATCHED, tag=def_tag)
if bool(def_entry.takes_value) != bool(placeholder):
return ErrorHandler.format_error(
TemporalErrors.ONSET_PLACEHOLDER_WRONG, tag=def_tag, has_placeholder=bool(def_entry.takes_value)
)
return []