from hed.models.definition_entry import DefinitionEntry
from hed.models.hed_string import HedString
from hed.errors.error_types import DefinitionErrors
from hed.errors.error_reporter import ErrorHandler
from hed.models.model_constants import DefTagNames
from hed.schema.hed_schema_constants import HedKey
[docs]class DefinitionDict:
""" Gathers definitions from a single source.
"""
[docs] def __init__(self, def_dicts=None, hed_schema=None):
""" Definitions to be considered a single source.
Parameters:
def_dicts (str or list or DefinitionDict): DefDict or list of DefDicts/strings or
a single string whose definitions should be added.
hed_schema(HedSchema or None): Required if passing strings or lists of strings, unused otherwise.
:raises TypeError:
- Bad type passed as def_dicts
"""
self.defs = {}
self._label_tag_name = DefTagNames.DEF_KEY
self._issues = []
if def_dicts:
self.add_definitions(def_dicts, hed_schema)
[docs] def add_definitions(self, def_dicts, hed_schema=None):
""" Add definitions from dict(s) to this dict.
Parameters:
def_dicts (list, DefinitionDict, or dict): DefinitionDict or list of DefinitionDicts/strings/dicts whose
definitions should be added.
Note dict form expects DefinitionEntries in the same form as a DefinitionDict
hed_schema(HedSchema or None): Required if passing strings or lists of strings, unused otherwise.
:raises TypeError:
- Bad type passed as def_dicts
"""
if not isinstance(def_dicts, list):
def_dicts = [def_dicts]
for def_dict in def_dicts:
if isinstance(def_dict, (DefinitionDict, dict)):
self._add_definitions_from_dict(def_dict)
elif isinstance(def_dict, str) and hed_schema:
self.check_for_definitions(HedString(def_dict, hed_schema))
elif isinstance(def_dict, list) and hed_schema:
for definition in def_dict:
self.check_for_definitions(HedString(definition, hed_schema))
else:
raise TypeError(f"Invalid type '{type(def_dict)}' passed to DefinitionDict")
def _add_definition(self, def_tag, def_value):
if def_tag in self.defs:
error_context = self.defs[def_tag].source_context
self._issues += ErrorHandler.format_error_from_context(DefinitionErrors.DUPLICATE_DEFINITION,
error_context=error_context, def_name=def_tag)
else:
self.defs[def_tag] = def_value
def _add_definitions_from_dict(self, def_dict):
""" Add the definitions found in the given definition dictionary to this mapper.
Parameters:
def_dict (DefinitionDict or dict): DefDict whose definitions should be added.
"""
for def_tag, def_value in def_dict.items():
self._add_definition(def_tag, def_value)
[docs] def get(self, def_name):
""" Get the definition entry for the definition name.
Not case-sensitive
Parameters:
def_name (str): Name of the definition to retrieve.
Returns:
DefinitionEntry: Definition entry for the requested definition.
"""
return self.defs.get(def_name.lower())
def __iter__(self):
return iter(self.defs)
def __len__(self):
return len(self.defs)
[docs] def items(self):
""" Returns the dictionary of definitions
Alias for .defs.items()
Returns:
def_entries({str: DefinitionEntry}): A list of definitions
"""
return self.defs.items()
@property
def issues(self):
"""Returns issues about duplicate definitions."""
return self._issues
[docs] def check_for_definitions(self, hed_string_obj, error_handler=None):
""" Check string for definition tags, adding them to self.
Parameters:
hed_string_obj (HedString): A single hed string to gather definitions from.
error_handler (ErrorHandler or None): Error context used to identify where definitions are found.
Returns:
list: List of issues encountered in checking for definitions. Each issue is a dictionary.
"""
def_issues = []
for definition_tag, group in hed_string_obj.find_top_level_tags(anchor_tags={DefTagNames.DEFINITION_KEY}):
group_tag, new_def_issues = self._find_group(definition_tag, group, error_handler)
def_tag_name, def_takes_value = self._strip_value_placeholder(definition_tag.extension)
if "/" in def_tag_name or "#" in def_tag_name:
new_def_issues += ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.INVALID_DEFINITION_EXTENSION,
tag=definition_tag,
def_name=def_tag_name)
if new_def_issues:
def_issues += new_def_issues
continue
new_def_issues = self._validate_contents(definition_tag, group_tag, error_handler)
new_def_issues += self._validate_placeholders(def_tag_name, group_tag, def_takes_value, error_handler)
if new_def_issues:
def_issues += new_def_issues
continue
new_def_issues, context = self._validate_name_and_context(def_tag_name, error_handler)
if new_def_issues:
def_issues += new_def_issues
continue
self.defs[def_tag_name.lower()] = DefinitionEntry(name=def_tag_name, contents=group_tag,
takes_value=def_takes_value,
source_context=context)
return def_issues
def _strip_value_placeholder(self, def_tag_name):
def_takes_value = def_tag_name.lower().endswith("/#")
if def_takes_value:
def_tag_name = def_tag_name[:-len("/#")]
return def_tag_name, def_takes_value
def _validate_name_and_context(self, def_tag_name, error_handler):
if error_handler:
context = error_handler.get_error_context_copy()
else:
context = []
new_def_issues = []
if def_tag_name.lower() in self.defs:
new_def_issues += ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.DUPLICATE_DEFINITION,
def_name=def_tag_name)
return new_def_issues, context
def _validate_placeholders(self, def_tag_name, group, def_takes_value, error_handler):
new_issues = []
placeholder_tags = []
tags_with_issues = []
if group:
for tag in group.get_all_tags():
count = str(tag).count("#")
if count:
placeholder_tags.append(tag)
if count > 1:
tags_with_issues.append(tag)
if tags_with_issues:
new_issues += ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.WRONG_NUMBER_PLACEHOLDER_TAGS,
def_name=def_tag_name,
tag_list=tags_with_issues,
expected_count=1 if def_takes_value else 0)
if (len(placeholder_tags) == 1) != def_takes_value:
new_issues += ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.WRONG_NUMBER_PLACEHOLDER_TAGS,
def_name=def_tag_name,
tag_list=placeholder_tags,
expected_count=1 if def_takes_value else 0)
return new_issues
if def_takes_value:
placeholder_tag = placeholder_tags[0]
if not placeholder_tag.is_takes_value_tag():
new_issues += ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.PLACEHOLDER_NO_TAKES_VALUE,
def_name=def_tag_name,
placeholder_tag=placeholder_tag)
return new_issues
def _find_group(self, definition_tag, group, error_handler):
# initial validation
groups = group.groups()
issues = []
if len(groups) > 1:
issues += \
ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.WRONG_NUMBER_GROUPS,
def_name=definition_tag.extension, tag_list=groups)
elif len(groups) == 0:
issues += \
ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.NO_DEFINITION_CONTENTS,
def_name=definition_tag.extension)
if len(group.tags()) != 1:
issues += \
ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.WRONG_NUMBER_TAGS,
def_name=definition_tag.extension,
tag_list=[tag for tag in group.tags()
if tag is not definition_tag])
group_tag = groups[0] if groups else None
return group_tag, issues
def _validate_contents(self, definition_tag, group, error_handler):
issues = []
if group:
def_keys = {DefTagNames.DEF_KEY, DefTagNames.DEF_EXPAND_KEY, DefTagNames.DEFINITION_KEY}
for def_tag in group.find_tags(def_keys, recursive=True, include_groups=0):
issues += ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.DEF_TAG_IN_DEFINITION,
tag=def_tag,
def_name=definition_tag.extension)
for tag in group.get_all_tags():
if tag.has_attribute(HedKey.Unique) or tag.has_attribute(HedKey.Required):
issues += ErrorHandler.format_error_with_context(error_handler,
DefinitionErrors.BAD_PROP_IN_DEFINITION,
tag=tag,
def_name=definition_tag.extension)
return issues
[docs] def construct_def_tag(self, hed_tag):
""" Identify def/def-expand tag contents in the given HedTag.
Parameters:
hed_tag(HedTag): The hed tag to identify definition contents in
"""
# Finish tracking down why parent is set incorrectly on def tags sometimes
# It should be ALWAYS set
if hed_tag.short_base_tag in {DefTagNames.DEF_ORG_KEY, DefTagNames.DEF_EXPAND_ORG_KEY}:
save_parent = hed_tag._parent
def_contents = self._get_definition_contents(hed_tag)
hed_tag._parent = save_parent
if def_contents is not None:
hed_tag._expandable = def_contents
hed_tag._expanded = hed_tag.short_base_tag == DefTagNames.DEF_EXPAND_ORG_KEY
def _get_definition_contents(self, def_tag):
""" Get the contents for a given def tag.
Does not validate at all.
Parameters:
def_tag (HedTag): Source hed tag that may be a Def or Def-expand tag.
Returns:
def_contents: HedGroup
The contents to replace the previous def-tag with.
"""
tag_label, _, placeholder = def_tag.extension.partition('/')
label_tag_lower = tag_label.lower()
def_entry = self.defs.get(label_tag_lower)
if def_entry is None:
# Could raise an error here?
return None
def_contents = def_entry.get_definition(def_tag, placeholder_value=placeholder)
return def_contents
[docs] @staticmethod
def get_as_strings(def_dict):
""" Convert the entries to strings of the contents
Parameters:
def_dict(DefinitionDict or dict): A dict of definitions
Returns:
dict(str: str): definition name and contents
"""
if isinstance(def_dict, DefinitionDict):
def_dict = def_dict.defs
return {key: str(value.contents) for key, value in def_dict.items()}