Source code for mlx.traceability.traceable_item

'''
Storage classes for traceable item
'''

import re

from natsort import natsorted

from .traceability_exception import TraceabilityException
from .traceable_base_class import TraceableBaseClass


[docs]class TraceableItem(TraceableBaseClass): ''' Storage for a traceable documentation item ''' STRING_TEMPLATE = 'Item {identification}\n' defined_attributes = {} def __init__(self, item_id, placeholder=False, **kwargs): ''' Initializes a new traceable item Args: item_id (str): Item identifier. placeholder (bool): Internal use only. ''' super().__init__(item_id, **kwargs) self.explicit_relations = {} self.implicit_relations = {} self.attributes = {} self.attribute_order = [] self._is_placeholder = placeholder
[docs] def update(self, other): ''' Updates item with other object. Stores the sum of both objects. Args: other (TraceableItem): Other TraceableItem which is the source for the update. ''' super(TraceableItem, self).update(other) self._add_relations(self.explicit_relations, other.explicit_relations) self._add_relations(self.implicit_relations, other.implicit_relations) # Remainder of fields: update if they improve the quality of the item for attr in other.attributes: self.add_attribute(attr, other.attributes[attr], False) if not other.is_placeholder: self._is_placeholder = False
@property def is_placeholder(self): ''' bool: True if this item is a placeholder; False otherwise ''' return self._is_placeholder @property def all_relations(self): ''' generator: Yields a relationship and the corresponding targets, both naturally sorted. ''' for relation in natsorted({**self.explicit_relations, **self.implicit_relations}): targets = set() if relation in self.explicit_relations: targets.update(self.explicit_relations[relation]) if relation in self.implicit_relations: targets.update(self.implicit_relations[relation]) if targets: yield relation, natsorted(targets) @staticmethod def _add_relations(relations_of_self, relations_of_other): ''' Adds all relations from other item to own relations. Args: relations_of_self (dict): Dictionary used to add relations to. relations_of_other (dict): Dictionary used to fetch relations from. ''' for relation in relations_of_other: if relation not in relations_of_self: relations_of_self[relation] = [] relations_of_self[relation].extend(relations_of_other[relation])
[docs] def is_linked(self, relationships, target_regex): ''' Checks if item is linked with any of the forwards relationships to a target matching the regex pattern Args: relationships (iterable): Forward relationships (str) target_regex (str/re.Pattern): Regular expression pattern or object Returns: bool: True if linked; False otherwise ''' for rel in relationships: for target in self.yield_targets(rel): try: match = target_regex.match(target) except AttributeError: match = re.match(target_regex, target) if match: return True return False
[docs] def add_target(self, relation, target, implicit=False): ''' Adds a relation to another traceable item. Note: using this API, the automatic reverse relation is not created. Adding the relation through the TraceableItemCollection class performs the adding of automatic reverse relations. Args: relation (str): Name of the relation. target (str): Item identification of the targeted traceable item. implicit (bool): If True, an explicitly expressed relation is added here. If false, an implicite (e.g. automatic reverse) relation is added here. ''' # When target is the item itself, it is an error: no circular relationships if self.identifier == target: raise TraceabilityException('circular relationship {src} {rel} {tgt}'.format(src=self.identifier, rel=relation, tgt=target), self.docname) # When relation is already explicit, we shouldn't add. It is an error. if relation in self.explicit_relations and target in self.explicit_relations[relation]: raise TraceabilityException('duplicating {src} {rel} {tgt}'.format(src=self.identifier, rel=relation, tgt=target), self.docname) # When relation is already implicit, we shouldn't add. When relation-to-add is explicit, it should move # from implicit to explicit. elif relation in self.implicit_relations and target in self.implicit_relations[relation]: if implicit is False: self._remove_target(self.implicit_relations, relation, target) self._add_target(self.explicit_relations, relation, target) # Otherwise it is a new relation, and we add to the selected database else: database = self.implicit_relations if implicit else self.explicit_relations self._add_target(database, relation, target)
@staticmethod def _add_target(database, relation, target): ''' Adds a relation to another traceable item. Args: database (dict): Dictionary to add the relation to. relation (str): Name of the relation. target (str): Item identification of the targeted traceable item. ''' if relation not in database: database[relation] = [] if target not in database[relation]: database[relation].append(target) @staticmethod def _remove_target(database, relation, target): ''' Deletes a relation to another traceable item. Args: relation (str): Name of the relation. target (str): Item identification of the targeted traceable item. database (dict): Dictionary to remove the relation from. ''' if relation in database: if target in database[relation]: database[relation].remove(target)
[docs] def remove_targets(self, target_id, explicit=False, implicit=True, relations=set()): ''' Removes any relation to given target item. Args: target_id (str): Identification of the target items to remove. explicit (bool): If True, explicitly expressed relations to given target are removed. implicit (bool): If True, implicitly expressed relations to given target are removed. relations (set): Set of relations to remove; empty to take all into account. ''' source_databases = [] if explicit: source_databases.append(self.explicit_relations) if implicit: source_databases.append(self.implicit_relations) for database in source_databases: for relation in database: if target_id in database[relation] and (not relations or relation in relations): database[relation].remove(target_id)
[docs] def iter_targets(self, relation, explicit=True, implicit=True, sort=True): ''' Gets a list of targets to other traceable item(s), naturally sorted by default. Args: relation (str): Name of the relation. explicit (bool): If True, explicitly expressed relations are included in the returned list. implicit (bool): If True, implicitly expressed relations are included in the returned list. sort (bool): True if the relations should be sorted naturally, False if no sorting is needed Returns: list: List of targets to other traceable item(s), naturally sorted by default ''' targets = [] if explicit and relation in self.explicit_relations: targets.extend(self.explicit_relations[relation]) if implicit and relation in self.implicit_relations: targets.extend(self.implicit_relations[relation]) if sort: return natsorted(targets) return targets
[docs] def yield_targets(self, *relations, explicit=True, implicit=True): ''' Gets an iterable of targets to other traceable items. Args: relations (iter[str]): One or more names of relations. explicit (bool): If True, explicitly expressed relations are included. implicit (bool): If True, implicitly expressed relations are included. Returns: generator: Targets to other traceable items, unsorted ''' for relation in relations: if explicit and relation in self.explicit_relations: for target in self.explicit_relations[relation]: yield target if implicit and relation in self.implicit_relations: for target in self.implicit_relations[relation]: yield target
[docs] def yield_targets_sorted(self, *args, **kwargs): ''' Gets an iterable of targets to other traceable items, with natural sorting applied. ''' gen = self.yield_targets(*args, **kwargs) return natsorted(gen)
[docs] def iter_relations(self, sort=True): ''' Iterates over available relations: naturally sorted by default. Args: sort (bool): True if the relations should be sorted naturally, False if no sorting is needed Returns: list: List containing available relations in the item, naturally sorted by default ''' relations = list(self.explicit_relations) + list(self.implicit_relations) if sort: return natsorted(relations) return relations
[docs] @staticmethod def define_attribute(attr): ''' Defines an attribute that can be assigned to traceable items. Args: attr (TraceableAttribute): Attribute to be assigned. ''' TraceableItem.defined_attributes[attr.identifier] = attr
[docs] def add_attribute(self, attr, value, overwrite=True): ''' Adds an attribute key-value pair to the traceable item. Note: The given attribute value is compared against defined attribute possibilities. An exception is thrown when the attribute value doesn't match the defined regex. Args: attr (str): Name of the attribute. value (str): Value of the attribute. overwrite (bool): Overwrite existing attribute value, if any. ''' if not attr or value is None or attr not in TraceableItem.defined_attributes: raise TraceabilityException('item {item} has invalid attribute ({attr}={value})' .format(item=self.identifier, attr=attr, value=value), self.docname) if not TraceableItem.defined_attributes[attr].can_accept(value): raise TraceabilityException('item {item} attribute does not match defined attributes ({attr}={value})' .format(item=self.identifier, attr=attr, value=value), self.docname) if overwrite or attr not in self.attributes: self.attributes[attr] = value
[docs] def remove_attribute(self, attr): ''' Removes an attribute key-value pair from the traceable item. Args: attr (str): Name of the attribute. ''' if not attr: raise TraceabilityException('item {item}: cannot remove invalid attribute {attr}' .format(item=self.identifier, attr=attr), self.docname) del self.attributes[attr]
[docs] def get_attribute(self, attr): ''' Gets the value of an attribute from the traceable item. Args: attr (str): Name of the attribute. Returns: str: Value matching the given attribute key, or '' if attribute does not exist. ''' return self.attributes.get(attr, '')
[docs] def get_attributes(self, attrs): ''' Gets the values of a list of attributes from the traceable item. Args: attr (list): List of names of the attribute Returns: list: List of values of the given attributes, '' is used as value for each attribute that does not exist ''' return [self.get_attribute(attr) for attr in attrs]
[docs] def iter_attributes(self): ''' Iterates over available attributes. Sorted as configured by an attribute-sort directive, with the remaining attributes naturally sorted. Returns: list: Sorted list containing available attributes in the item. ''' sorted_attributes = [attr for attr in self.attribute_order if attr in self.attributes] sorted_attributes.extend(natsorted(set(self.attributes).difference(set(self.attribute_order)))) return sorted_attributes
def __str__(self, explicit=True, implicit=True): ''' Converts object to string. Args: explicit (bool) Returns: str: String representation of the item. ''' retval = TraceableItem.STRING_TEMPLATE.format(identification=self.identifier) retval += '\tPlaceholder: {placeholder}\n'.format(placeholder=self.is_placeholder) for attribute in self.attributes: retval += '\tAttribute {attribute} = {value}\n'.format(attribute=attribute, value=self.attributes[attribute]) if explicit: retval += self._relations_to_str(self.explicit_relations, 'Explicit') if implicit: retval += self._relations_to_str(self.implicit_relations, 'Implicit') return retval @staticmethod def _relations_to_str(relations, description): ''' Returns the string represtentation of the given relations. Args: relations (dict): Dictionary of relations. description (str): Description of the kind of relations. ''' retval = '' for relation in relations: retval += '\t{text} {relation}\n'.format(text=description, relation=relation) for tgtid in relations[relation]: retval += '\t\t{target}\n'.format(target=tgtid) return retval
[docs] def is_match(self, regex): ''' Checks if the item matches a given regular expression. Args: regex (str/re.Pattern): Regular expression pattern or object to match the given item against. Returns: bool: True if the given regex matches the item identification. ''' if regex == '': return True try: return regex.match(self.identifier) except AttributeError: return re.match(regex, self.identifier)
[docs] def attributes_match(self, attributes): ''' Checks if item matches a given set of attributes. Args: attributes (dict): Dictionary with attribute-regex pairs to match the given item against. Returns: bool: True if the given attributes match the item attributes. ''' for attr, regex in attributes.items(): if attr not in self.attributes: return False if regex == '': continue attribute_value = self.attributes[attr] try: if not regex.match(attribute_value): return False except AttributeError: if not re.match(regex, attribute_value): return False return True
[docs] def has_relations(self, relations): ''' Checks if the item has every relationship in given list. Args: relations (list): List of relations. Returns: bool: True if the item has every relationship in given list of list is empty, False otherwise. ''' return set(relations).issubset(self.iter_relations(sort=False))
[docs] def to_dict(self): ''' Exports item to a dictionary. Returns: dict: Dictionary representation of the object. ''' data = {} if not self.is_placeholder: data = super(TraceableItem, self).to_dict() data['attributes'] = self.attributes data['targets'] = {} for relation in self.iter_relations(): tgts = self.iter_targets(relation) if tgts: data['targets'][relation] = tgts return data
[docs] def self_test(self): ''' Performs self-test on collection content. Raises: TraceabilityException: Item is not defined. TraceabilityException: Item has an invalid attribute value. TraceabilityException: Duplicate target found for item. ''' super().self_test() # Item should not be a placeholder if self.is_placeholder: raise TraceabilityException('item {item} is not defined'.format(item=self.identifier), self.docname) # Item's attributes should be valid, empty string is allowed for attribute in self.iter_attributes(): value = self.attributes[attribute] if value is None or not TraceableItem.defined_attributes[attribute].can_accept(value): raise TraceabilityException('item {item} has invalid attribute value for {attribute}' .format(item=self.identifier, attribute=attribute)) # Targets should have no duplicates for relation in self.iter_relations(sort=False): tgts = self.iter_targets(relation, sort=False) cnt_duplicate = len(tgts) - len(set(tgts)) if cnt_duplicate: raise TraceabilityException('{cnt} duplicate target(s) found for {item} {relation})' .format(cnt=cnt_duplicate, item=self.identifier, relation=relation), self.docname)