Source code for delphin.tfs


"""
Basic classes for modeling feature structures.
"""

# Default modules need to import the PyDelphin version
from delphin.__about__ import __version__  # noqa: F401
from delphin.exceptions import PyDelphinException
from delphin.hierarchy import MultiHierarchy


class TFSError(PyDelphinException):
    """Raised on invalid feature structure operations."""


[docs] class FeatureStructure: """ A feature structure. This class manages the access of nested features using dot-delimited notation (e.g., `SYNSEM.LOCAL.CAT.HEAD`). Args: featvals (dict, list): a mapping or iterable of feature paths to feature values """ __slots__ = ('_avm', '_feats') def __init__(self, featvals=None): self._avm = {} self._feats = [] if isinstance(featvals, dict): featvals = featvals.items() for feat, val in list(featvals or []): self[feat] = val @classmethod def _default(cls): return cls(None) def __repr__(self): return '<{} object at {}>'.format(self.__class__.__name__, id(self)) def __eq__(self, other): if not isinstance(other, FeatureStructure): return NotImplemented return self._avm == other._avm def __setitem__(self, key, val): avm = self._avm subkeys = key.split('.', 1) subkey = subkeys[0].upper() if subkey not in avm: self._feats.append(subkey) if len(subkeys) == 1: avm[subkey] = val else: if subkey in avm: subdef = avm[subkey] if not hasattr(subdef, '__setitem__'): raise TFSError( f'{type(subdef)!r} object at feature ' f'{subkey} does not support item assignment') else: subdef = avm[subkey] = self._default() subdef[subkeys[1]] = val def __getitem__(self, key): first, _, remainder = key.partition('.') val = self._avm[first.upper()] if remainder: val = val[remainder] return val def __delitem__(self, key): first, _, remainder = key.partition('.') if remainder: fs = self._avm[first.upper()] del fs[remainder] else: del self._avm[first.upper()] def __contains__(self, key): subkeys = key.split('.', 1) subkey = subkeys[0].upper() if subkey in self._avm: if len(subkeys) == 2: return subkeys[1] in self._avm[subkey] else: return True return False
[docs] def get(self, key, default=None): """ Return the value for *key* if it exists, otherwise *default*. """ try: val = self[key] except KeyError: val = default return val
def _is_notable(self): """ Notability determines if the FeatureStructure should be listed as the value of a feature or if the feature should just "pass through" its avm to get the next value. A notable FeatureStructure is one with more than one sub-feature. """ return self._avm is None or len(self._avm) != 1
[docs] def features(self, expand=False): """ Return the list of tuples of feature paths and feature values. Args: expand (bool): if `True`, expand all feature paths Example: >>> fs = FeatureStructure([('A.B', 1), ('A.C', 2)]) >>> fs.features() [('A', <FeatureStructure object at ...>)] >>> fs.features(expand=True) [('A.B', 1), ('A.C', 2)] """ fs = [] if self._avm is not None: if len(self._feats) == len(self._avm): feats = self._feats else: feats = list(self._avm) for feat in feats: val = self._avm[feat] if isinstance(val, FeatureStructure): if not expand and val._is_notable(): fs.append((feat, val)) else: for subfeat, subval in val.features(expand=expand): fs.append(('{}.{}'.format(feat, subfeat), subval)) else: fs.append((feat, val)) return fs
[docs] class TypedFeatureStructure(FeatureStructure): """ A typed :class:`FeatureStructure`. Args: type (str): type name featvals (dict, list): a mapping or iterable of feature paths to feature values """ __slots__ = '_type' def __init__(self, type, featvals=None): self._type = type super().__init__(featvals) def __repr__(self): return '<TypedFeatureStructure object ({}) at {}>'.format( self.type, id(self) ) def __eq__(self, other): if not isinstance(other, TypedFeatureStructure): return NotImplemented return self._type == other._type and self._avm == other._avm @property def type(self): """The type assigned to the feature structure.""" return self._type @type.setter def type(self, value): self._type = value
[docs] class TypeHierarchy(MultiHierarchy): """ A Type Hierarchy. Type hierarchies have certain properties, such as a unique top node, multiple inheritance, case insensitivity, and unique greatest-lower-bound (glb) types. Note: Checks for unique glbs is not yet implemented. TypeHierarchies may be constructed when instantiating the class or via the :meth:`update` method using a dictionary mapping type names to node values, or one-by-one using dictionary-like access. In both cases, the node values may be an individual parent name, an iterable of parent names, or a :class:`TypeHierarchyNode` object. Retrieving a node via dictionary access on the typename returns a :class:`TypeHierarchyNode` regardless of the method used to create the node. >>> th = TypeHierarchy('*top*', {'can-fly': '*top*'}) >>> th.update({'can-swim': '*top*', 'can-walk': '*top*'}) >>> th['butterfly'] = ('can-fly', 'can-walk') >>> th['duck'] = TypeHierarchyNode( ... ('can-fly', 'can-swim', 'can-walk'), ... data='some info relating to ducks...') >>> th['butterfly'].data = 'some info relating to butterflies' In some ways the TypeHierarchy behaves like a dictionary, but it is not a subclass of :py:class:`dict` and does not implement all its methods. Also note that some methods ignore the top node, which make certain actions easier: >>> th = TypeHierarchy('*top*', {'a': '*top*', 'b': 'a', 'c': 'a'}) >>> len(th) 3 >>> list(th) ['a', 'b', 'c'] >>> TypeHierarchy('*top*', dict(th.items())) == th True But others do not ignore the top node, namely those where you can request it specifically: >>> '*top*' in th True >>> th['*top*'] <TypeHierarchyNode ... > Args: top (str): unique top type hierarchy (dict): mapping of `{child: node}` (see description above concerning the `node` values) Attributes: top: the hierarchy's top type """ def __init__(self, top, hierarchy=None, data=None, normalize_identifier=None): if not normalize_identifier: normalize_identifier = str.lower super().__init__(top, hierarchy=hierarchy, data=data, normalize_identifier=normalize_identifier)