Source code for graf.annotations

# graf-python: Python GrAF API
#
# Copyright (C) 2014 American National Corpus
# Author: Keith Suderman <suderman@cs.vassar.edu> (Original API)
#         Stephen Matysik <smatysik@gmail.com> (Conversion to Python)
# URL: <http://www.anc.org/>
# For license information, see LICENSE.TXT
#

import copy

[docs]class Annotation(object): """ An Annotation is the artifact being annotated. An annotation is a labelled feature structure. The annotation class/interface also provides convenience methods for setting and getting values from a feature structure. """ __slots__ = ('id', 'label', 'features', 'aspace', 'element') _ninsts = -1
[docs] def __init__(self, label, features=None, id=None): """Construct a new C{Annotation}. :param label: C{str} :param features: C{list} of C{Feature} objects """ self.id = id if id is not None else self._next_id() self.label = label if not isinstance(features, FeatureStructure): features = FeatureStructure(items=features) self.features = features self.aspace = None self.element = None
@classmethod def _next_id(cls): cls._ninsts += 1 return 'a-%d' % cls._ninsts def __repr__(self): return "Annotation(%r, %r)" % (self.label, self.id) def __eq__(self, id): return self.id == id
#TODO: perhaps delegate __*item__, etc. methods to features class AnnotationList(object): """ A collection of Annotations which marks a field on the annotation object indicating its possession. """ __slots__ = ('_elements', '_set_owner') def __init__(self, owned_by, owner_field): self._elements = [] self._set_owner = lambda ann: setattr(ann, owner_field, owned_by) def __len__(self): return len(self._elements) def __iter__(self): return iter(self._elements) def __repr__(self): return repr(self._elements) def add(self, ann): """Adds a C{Annotation} to this C{AnnotationSpace}. :param a: Annotation """ #if ann not in self._elements: self._elements.append(ann) self._set_owner(ann) def create(self, label): """Creates a new annotation with specified label, adds it to this annotation set, and returns the new annotation. :param label: str :return: Annotation """ ann = Annotation(label) self.add(ann) return ann def select(self, label=None, fs=None, aspace=None): """Generates Annotation objects having the given label and features subsumed by the given FeatureStructure. Parameters ---------- label : str fs : FeatureStructure aspace : an AnnotationSpace name Returns ------- gen : generator of Annotation """ filters = self._build_filters(label, fs, aspace) return (ann for ann in self._elements if all(fn(ann) for fn in filters)) def select_not(self, label=None, fs=None, aspace=None): """ Generates those annotations that would not be returned by select() with the same arguments. """ filters = self._build_filters(label, fs, aspace) return (ann for ann in self._elements if not all(fn(ann) for fn in filters)) @staticmethod def _build_filters(label=None, fs=None, aspace=None): res = [] if aspace is not None: res.append(lambda ann: ann.aspace is not None and ann.aspace.name == aspace) if label is not None: res.append(lambda ann: ann.label == label) if fs is not None: res.append(lambda ann: fs.subsumes(ann.features)) return res def get_first(self, label=None, fs=None, aspace=None): try: return next(self.select(label, fs, aspace)) except StopIteration: raise ValueError('No annotations match those criteria')
[docs]class AnnotationSpace(AnnotationList): """ A collection of Annotations. Each AnnotationSpace has a name (C{Str}) and a type (C{URI}) and a set of annotations. """ __slots__ = ('as_id')
[docs] def __init__(self, as_id): """Constructor for C{AnnotationSpace} :param name: C{str} :param type: C{str} """ super(AnnotationSpace, self).__init__(self, 'aspace') self.as_id = as_id
def __copy__(self): res = AnnotationSpace(self.as_id) res.annotations = self.annotations[:] return res def __repr__(self): return "AnnotationSpace(%r)" % (self.as_id)
[docs] def remove(self, ann): """Remove the given C{Annotation} object. :param a: Annotation """ try: return self._elements.remove(ann) except ValueError: print('Error: Annotation not in set')
[docs] def remove_where(self, label, fs=None): """Remove the C{Annotation}s with the given label in the given C{FeatureStructure} :param label: C{str} :param fs: C{FeatureStructure} """ self._elements = list(self.select_not(label, fs))
[docs]class FeatureStructure(object): """ A dict of key -> feature, where feature is either a string or another FeatureStructure. A FeatureStructure may also have a type. When key is a tuple of names, or a string of names joined by '/', it is interpreted as the path to a nested feature structure. Additionally, a FeatureStructure defines the operations 'subsumes' and 'unify'. """ __slots__ = ('type', '_elements')
[docs] def __init__(self, type_var=None, items=None): """Constructor for C{FeatureStructure}. :param type: C{str} """ self.type = type_var self._elements = {} if items: self.update(items)
def __len__(self): return len(self._elements) def __repr__(self): return "<FeatureStructure(%r) with %d elements>" % (self.type, len(self)) def __copy__(self): res = FeatureStructure(self.type) res._elements = self._elements.copy() return res copy = __copy__ def __deepcopy__(self): res = FeatureStructure(self.type) res._elements = copy.deepcopy(self._elements) return res def __iter__(self): return iter(self._elements) if hasattr(dict, 'iterkeys'): # Python 2.x def iterkeys(self): return self._elements.iterkeys() def iteritems(self): return self._elements.iteritems() if hasattr(dict, 'viewkeys'): # Python 2.7+ def viewkeys(self): return self._elements.viewkeys() def viewitems(self): return self._elements.viewitems() def keys(self): return self._elements.keys() def items(self): return self._elements.items() def _resolve_fs(self, path, create=False): """ Resolves a list of keys to this or a descendent feature structure. """ fs = self for name in path: try: fs = fs._elements[name] except KeyError: if create: fs = fs._elements[name] = FeatureStructure() else: fs = None if not isinstance(fs, FeatureStructure): raise KeyError('Could not resolve feature structure for path %r. Got %r' % (path, fs)) return fs def _parse_key(self, key, create=False): try: key = key.strip('/').split('/') except AttributeError: # assume key is already list of path elements pass return self._resolve_fs(key[:-1], create), key[-1] def __contains__(self, key): try: fs, key = self._parse_key(key) except KeyError: return False return key in fs._elements def __getitem__(self, key): fs, key = self._parse_key(key) return fs._elements[key] def get(self, key, default=None): try: return self[key] except KeyError: return default
[docs] def get_fs(self, key): """Returns the value corresponding to key if it is a FeatureStructure, and otherwise throws a ValueError""" val = self[key] if not isinstance(val, FeatureStructure): raise ValueError('Value for key %r is not a FeatureStructure' % key) return val
[docs] def get_value(self, key): """Returns the value corresponding to key but throws a ValueError if it is a FeatureStructure""" val = self[key] if isinstance(val, FeatureStructure): raise ValueError('Value for key %r is a FeatureStructure' % key) return val
def __setitem__(self, key, val): fs, key = self._parse_key(key, create=True) fs._elements[key] = val def setdefault(self, key, default): fs, key = self._parse_key(key, create=True) return fs._elements.setdefault(key, default) def update(self, other): if hasattr(other, 'items'): other = other.items() for key, value in other: self[key] = value def __delitem__(self, key): fs, key = self._parse_key(key) del fs._elements[key] def pop(self, key, default=None): try: fs, key = self._parse_key(key) return fs._elements.pop(key, default) except KeyError: return default def __eq__(self, other): """ Equivalence is equivalent types (????) """ try: return self.type == other.type except AttributeError: return False def subsumes(self, other): for key, val in self.items(): try: oval = other._elements[key] except KeyError: return False if isinstance(val, FeatureStructure) and isinstance(oval, FeatureStructure): if not val.subsumes(oval): return False elif val != oval: return False return True def unify(self, other): if self.type != other.type and self.type is not None and other.type is not None: raise ValueError('Cannot unify feature structues of different types: %r and %r' % (self.type, other.type)) res = copy.deepcopy(self) for name, oval in other.items(): if name not in res._elements: res._elements[name] = copy.deepcopy(oval) continue val = res._elements[name] if isinstance(val, FeatureStructure) and isinstance(oval, FeatureStructure): res._elements[name] = val.unify(oval) elif val != oval: raise ValueError('Name %r exists but value %r != %r in unification' % (name, val, oval)) return res