from hed.schema.hed_schema_constants import HedKey
import copy
[docs]class HedTag:
""" A single HED tag.
Notes:
- HedTag is a smart class in that it keeps track of its original value and positioning
as well as pointers to the relevant HED schema information, if relevant.
"""
[docs] def __init__(self, hed_string, hed_schema, span=None, def_dict=None):
""" Creates a HedTag.
Parameters:
hed_string (str): Source hed string for this tag.
hed_schema (HedSchema): A parameter for calculating canonical forms on creation.
span (int, int): The start and end indexes of the tag in the hed_string.
def_dict(DefinitionDict or None): The def dict to use to identify def/def expand tags.
"""
self._hed_string = hed_string
if span is None:
span = (0, len(hed_string))
# This is the span into the original hed string for this tag
self.span = span
# If this is present, use this as the org tag for most purposes.
# This is not generally used anymore, but you can use it to replace a tag in place.
self._tag = None
self._namespace = self._get_schema_namespace(self.org_tag)
# This is the schema this tag was converted to.
self._schema = None
self._schema_entry = None
self._extension_value = ""
self._parent = None
self._expandable = None
self._expanded = False
self.tag_terms = None # tuple of all the terms in this tag Lowercase.
self._calculate_to_canonical_forms(hed_schema)
if def_dict:
def_dict.construct_def_tag(self)
[docs] def copy(self):
""" Return a deep copy of this tag.
Returns:
HedTag: The copied group.
"""
save_parent = self._parent
self._parent = None
return_copy = copy.deepcopy(self)
self._parent = save_parent
return return_copy
@property
def schema_namespace(self):
""" Library namespace for this tag if one exists.
Returns:
namespace (str): The library namespace, including the colon.
"""
return self._namespace
@property
def short_tag(self):
""" Short form including value or extension.
Returns:
short_tag (str): The short form of the tag, including value or extension.
"""
if self._schema_entry:
return f"{self._namespace}{self._schema_entry.short_tag_name}{self._extension_value}"
return str(self)
@property
def base_tag(self):
""" Long form without value or extension.
Returns:
base_tag (str): The long form of the tag, without value or extension.
"""
if self._schema_entry:
return self._schema_entry.long_tag_name
return str(self)
@property
def short_base_tag(self):
""" Short form without value or extension
Returns:
base_tag (str): The short non-extension port of a tag.
Notes:
- ParentNodes/Def/DefName would return just "Def".
"""
if self._schema_entry:
return self._schema_entry.short_tag_name
return str(self)
@short_base_tag.setter
def short_base_tag(self, new_tag_val):
""" Change base tag, leaving extension or value.
Parameters:
new_tag_val (str): The new short_base_tag for this tag.
:raises ValueError:
- If the tag wasn't already identified
Note:
- Generally this is used to swap def to def-expand.
"""
if self._schema_entry:
tag_entry = None
if self._schema:
if self.is_takes_value_tag():
new_tag_val = new_tag_val + "/#"
tag_entry = self._schema.get_tag_entry(new_tag_val, schema_namespace=self.schema_namespace)
self._schema_entry = tag_entry
else:
raise ValueError("Cannot set unidentified tags")
@property
def org_base_tag(self):
""" Original form without value or extension.
Returns:
base_tag (str): The original form of the tag, without value or extension.
Notes:
- Warning: This could be empty if the original tag had a name_prefix prepended.
e.g. a column where "Label/" is prepended, thus the column value has zero base portion.
"""
if self._schema_entry:
extension_len = len(self._extension_value)
if not extension_len:
return self.tag
org_len = len(self.tag)
if org_len == extension_len:
return ""
return self.tag[:org_len - extension_len]
return str(self)
[docs] def tag_modified(self):
""" Return true if tag has been modified from original.
Returns:
bool: Return True if the tag is modified.
Notes:
- Modifications can include adding a column name_prefix.
"""
return bool(self._tag)
@property
def tag(self):
""" Returns the tag.
Returns the original tag if no user form set.
Returns:
tag (str): The custom set user form of the tag.
"""
if self._tag:
return self._tag
return self.org_tag
@tag.setter
def tag(self, new_tag_val):
""" Allow you to overwrite the tag output text.
Parameters:
new_tag_val (str): New (implicitly long form) of tag to set.
Notes:
- You probably don't actually want to call this.
"""
self._tag = new_tag_val
self._schema_entry = None
self._calculate_to_canonical_forms(self._schema)
@property
def extension(self):
""" Get the extension or value of tag
Generally this is just the portion after the last slash.
Returns an empty string if no extension or value.
Returns:
str: The tag name.
Notes:
- This tag must have been computed first.
"""
if self._extension_value:
return self._extension_value[1:]
return ""
@extension.setter
def extension(self, x):
self._extension_value = f"/{x}"
@property
def long_tag(self):
""" Long form including value or extension.
Returns:
str: The long form of this tag.
"""
if self._schema_entry:
return f"{self._namespace}{self._schema_entry.long_tag_name}{self._extension_value}"
return str(self)
@property
def org_tag(self):
""" Return the original unmodified tag.
Returns:
str: The original unmodified tag.
"""
return self._hed_string[self.span[0]:self.span[1]]
@property
def expanded(self):
"""Returns if this is currently expanded or not.
Will always be false unless expandable is set. This is primarily used for Def/Def-expand tags at present.
Returns:
bool: Returns true if this is currently expanded
"""
return self._expanded
@property
def expandable(self):
"""Returns what this expands to
This is primarily used for Def/Def-expand tags at present.
Returns:
HedGroup or HedTag or None: Returns the expanded form of this tag
"""
return self._expandable
[docs] def is_column_ref(self):
""" Returns if this tag is a column reference from a sidecar.
You should only see these if you are directly accessing sidecar strings, tools should remove them otherwise.
Returns:
bool: Returns True if this is a column ref
"""
return self.org_tag.startswith('{') and self.org_tag.endswith('}')
def __str__(self):
""" Convert this HedTag to a string.
Returns:
str: The original tag if we haven't set a new tag.(e.g. short to long).
"""
if self._schema_entry:
return self.short_tag
if self._tag:
return self._tag
return self._hed_string[self.span[0]:self.span[1]]
[docs] def lower(self):
""" Convenience function, equivalent to str(self).lower(). """
return str(self).lower()
def _calculate_to_canonical_forms(self, hed_schema):
""" Update internal state based on schema.
Parameters:
hed_schema (HedSchema or HedSchemaGroup): The schema to use to validate this tag
Returns:
list: A list of issues found during conversion. Each element is a dictionary.
"""
tag_entry, remainder, tag_issues = hed_schema.find_tag_entry(self, self.schema_namespace)
self._schema_entry = tag_entry
self._schema = hed_schema
if self._schema_entry:
self.tag_terms = self._schema_entry.tag_terms
if remainder:
self._extension_value = remainder
else:
self.tag_terms = tuple()
return tag_issues
[docs] def get_stripped_unit_value(self):
""" Return the extension divided into value and units, if the units are valid.
Returns:
stripped_unit_value (str): The extension portion with the units removed.
unit (str or None): None if no valid unit found.
Examples:
'Duration/3 ms' will return '3'
"""
tag_unit_classes = self.unit_classes
stripped_value, unit, _ = self._get_tag_units_portion(tag_unit_classes)
if stripped_value:
return stripped_value, unit
return self.extension, None
[docs] def value_as_default_unit(self):
""" Returns the value converted to default units if possible.
Returns None if the units are invalid.(No default unit or invalid)
Returns:
value (float or None): The extension value as default units.
If there are not default units, returns None.
Examples:
'Duration/300 ms' will return .3
"""
tag_unit_classes = self.unit_classes
value, _, units = self.extension.rpartition(" ")
if not value:
stripped_value = units
unit_entry = self.default_unit
unit = unit_entry.name
else:
stripped_value, unit, unit_entry = self._get_tag_units_portion(tag_unit_classes)
if stripped_value:
if unit_entry.get_conversion_factor(unit) is not None:
return float(stripped_value) * unit_entry.get_conversion_factor(unit)
@property
def unit_classes(self):
""" Return a dict of all the unit classes this tag accepts.
Returns:
unit_classes (dict): A dict of unit classes this tag accepts.
Notes:
- Returns empty dict if this is not a unit class tag.
- The dictionary has unit name as the key and HedSchemaEntry as value.
"""
if self._schema_entry:
return self._schema_entry.unit_classes
return {}
@property
def value_classes(self):
""" Return a dict of all the value classes this tag accepts.
Returns:
dict: A dictionary of HedSchemaEntry value classes this tag accepts.
Notes:
- Returns empty dict if this is not a value class.
- The dictionary has unit name as the key and HedSchemaEntry as value.
"""
if self._schema_entry:
return self._schema_entry.value_classes
return {}
@property
def attributes(self):
""" Return a dict of all the attributes this tag has.
Returns empty dict if this is not a value tag.
Returns:
dict: A dict of attributes this tag has.
Notes:
- Returns empty dict if this is not a unit class tag.
- The dictionary has unit name as the key and HedSchemaEntry as value.
"""
if self._schema_entry:
return self._schema_entry.attributes
return {}
[docs] def tag_exists_in_schema(self):
""" Get the schema entry for this tag.
Returns:
bool: True if this tag exists.
Notes:
- This does NOT assure this is a valid tag.
"""
return bool(self._schema_entry)
[docs] def is_takes_value_tag(self):
""" Return true if this is a takes value tag.
Returns:
bool: True if this is a takes value tag.
"""
if self._schema_entry:
return self._schema_entry.has_attribute(HedKey.TakesValue)
return False
[docs] def is_unit_class_tag(self):
""" Return true if this is a unit class tag.
Returns:
bool: True if this is a unit class tag.
"""
if self._schema_entry:
return bool(self._schema_entry.unit_classes)
return False
[docs] def is_value_class_tag(self):
""" Return true if this is a value class tag.
Returns:
bool: True if this is a tag with a value class.
"""
if self._schema_entry:
return bool(self._schema_entry.value_classes)
return False
[docs] def is_basic_tag(self):
""" Return True if a known tag with no extension or value.
Returns:
bool: True if this is a known tag without extension or value.
"""
return bool(self._schema_entry and not self.extension)
[docs] def has_attribute(self, attribute):
""" Return true if this is an attribute this tag has.
Parameters:
attribute (str): Name of the attribute.
Returns:
bool: True if this tag has the attribute.
"""
if self._schema_entry:
return self._schema_entry.has_attribute(attribute)
return False
[docs] def get_tag_unit_class_units(self):
""" Get the unit class units associated with a particular tag.
Returns:
list: A list containing the unit class units associated with a particular tag or an empty list.
"""
units = []
unit_classes = self.unit_classes
for unit_class_entry in unit_classes.values():
units += unit_class_entry.units.keys()
return units
@property
def default_unit(self):
""" Get the default unit class unit for this tag.
Only a tag with a single unit class can have default units.
Returns:
unit(UnitEntry or None): the default unit entry for this tag, or None
"""
# todo: Make this cached
unit_classes = self.unit_classes.values()
if len(unit_classes) == 1:
first_unit_class_entry = list(unit_classes)[0]
default_unit = first_unit_class_entry.has_attribute(HedKey.DefaultUnits, return_value=True)
return first_unit_class_entry.units.get(default_unit, None)
[docs] def base_tag_has_attribute(self, tag_attribute):
""" Check to see if the tag has a specific attribute.
This is primarily used to check for things like TopLevelTag on Definitions and similar.
Parameters:
tag_attribute (str): A tag attribute.
Returns:
bool: True if the tag has the specified attribute. False, if otherwise.
"""
if not self._schema_entry:
return False
return self._schema_entry.base_tag_has_attribute(tag_attribute)
@staticmethod
def _get_schema_namespace(org_tag):
""" Finds the library namespace for the tag.
Parameters:
org_tag (str): A string representing a tag.
Returns:
str: Library namespace string or empty.
"""
first_slash = org_tag.find("/")
first_colon = org_tag.find(":")
if first_colon != -1:
if first_slash != -1 and first_colon > first_slash:
return ""
return org_tag[:first_colon + 1]
return ""
def _get_tag_units_portion(self, tag_unit_classes):
""" Check that this string has valid units and remove them.
Parameters:
tag_unit_classes (dict): Dictionary of valid UnitClassEntry objects for this tag.
Returns:
stripped_value (str or None): The value with the units removed.
This is filled in if there are no units as well.
unit (UnitEntry or None): The matching unit entry if one is found
"""
value, _, units = self.extension.rpartition(" ")
if not units:
return None, None, None
for unit_class_entry in tag_unit_classes.values():
all_valid_unit_permutations = unit_class_entry.derivative_units
possible_match = self._find_modifier_unit_entry(units, all_valid_unit_permutations)
if possible_match and not possible_match.has_attribute(HedKey.UnitPrefix):
return value, units, possible_match
# Repeat the above, but as a prefix
possible_match = self._find_modifier_unit_entry(value, all_valid_unit_permutations)
if possible_match and possible_match.has_attribute(HedKey.UnitPrefix):
return units, value, possible_match
return None, None, None
@staticmethod
def _find_modifier_unit_entry(units, all_valid_unit_permutations):
possible_match = all_valid_unit_permutations.get(units)
# If we have a match that's a unit symbol, we're done, return it.
if possible_match and possible_match.has_attribute(HedKey.UnitSymbol):
return possible_match
possible_match = all_valid_unit_permutations.get(units.lower())
# Unit symbols must match including case, a match of a unit symbol now is something like M becoming m.
if possible_match and possible_match.has_attribute(HedKey.UnitSymbol):
possible_match = None
return possible_match
[docs] def is_placeholder(self):
if "#" in self.org_tag or "#" in self._extension_value:
return True
return False
[docs] def replace_placeholder(self, placeholder_value):
""" If tag has a placeholder character(#), replace with value.
Parameters:
placeholder_value (str): Value to replace placeholder with.
"""
if self.is_placeholder():
if self._schema_entry:
self._extension_value = self._extension_value.replace("#", placeholder_value)
else:
self._tag = self.tag.replace("#", placeholder_value)
def __hash__(self):
if self._schema_entry:
return hash(
self._namespace + self._schema_entry.short_tag_name.lower() + self._extension_value.lower())
else:
return hash(self.lower())
def __eq__(self, other):
if self is other:
return True
if isinstance(other, str):
return self.lower() == other
if not isinstance(other, HedTag):
return False
if self.short_tag.lower() == other.short_tag.lower():
return True
if self.org_tag.lower() == other.org_tag.lower():
return True
return False
def __deepcopy__(self, memo):
# check if the object has already been copied
if id(self) in memo:
return memo[id(self)]
# create a new instance of HedTag class
new_tag = self.__class__.__new__(self.__class__)
new_tag.__dict__.update(self.__dict__)
# add the new object to the memo dictionary
memo[id(self)] = new_tag
# Deep copy the attributes that need it(most notably, we don't copy schema/schema entry)
new_tag._parent = copy.deepcopy(self._parent, memo)
new_tag._expandable = copy.deepcopy(self._expandable, memo)
new_tag._expanded = copy.deepcopy(self._expanded, memo)
return new_tag