Source code for cltoolkit.features.collection

import typing
import textwrap
import importlib
import collections

import attr
from pycldf.util import DictTuple
from clldutils import jsonlib

__all__ = ['Feature', 'FeatureCollection', 'get_callable']


[docs]def get_callable(s: typing.Union[str, dict, typing.Callable]) -> typing.Callable: """ A "feature function" can be specified in 3 ways: - as Python callable object - as string of dot-separated names, where the part up to the last dot is taken as Python \ module spec, and the last name as symbol to be looked up in this module - as `dict` with keys `class`, `args`, `kwargs`, where `class` is interpreted as above, and \ `args` and `kwargs` are passed into the imported class to initialize an instance, the \ `__call__` method of which will be used as "feature function". """ if callable(s): return s if isinstance(s, str): comps = s.split('.') return getattr(importlib.import_module('.'.join(comps[:-1])), comps[-1]) if isinstance(s, dict): return get_callable(s['class'])(*s.get('args') or [], **s.get('kwargs') or {}) raise ValueError(s)
[docs]@attr.s(repr=False) class Feature: """ :ivar id: `str` :ivar name: `str` :ivar function: `callable` .. seealso:: :func:`get_callable` """ id = attr.ib() name = attr.ib() function = attr.ib(converter=get_callable) type = attr.ib(default=None) note = attr.ib(default=None) categories = attr.ib(default=None) requires = attr.ib(default=None) def __attrs_post_init__(self): if hasattr(self.function, 'categories'): self.categories = self.function.categories if hasattr(self.function, 'rtype'): self.type = self.function.rtype func = getattr(self.function, '__call__', self.function) if hasattr(func, 'requires'): self.requires = func.requires def to_json(self) -> dict: def j(o, field=None): if field == 'type': return getattr(o, '__name__', str(o)) if isinstance(o, (list, tuple)): return [j(oo) for oo in o] if hasattr(o, 'to_json'): return o.to_json() if callable(o): comps = [o.__module__] if o.__module__ != 'builtins' else [] if type(o) == type(get_callable): comps.append(o.__name__) else: comps.append(o.__class__.__name__) res = '.'.join(comps) if type(o) == type(get_callable): return res return {'class': res} return o return collections.OrderedDict([ (f.name, j(getattr(self, f.name), field=f.name)) for f in attr.fields(self.__class__)]) @property def doc(self) -> str: return getattr(self.function, 'doc', None) or textwrap.dedent(self.function.__doc__ or '') def help(self): print(self.doc) def __call__(self, param): return self.function(param) def __repr__(self): return "<Feature " + self.id + ">"
[docs]class FeatureCollection(DictTuple): """ A collection of `Feature` instances. """
[docs] def dump(self, path): """ Dump feature specifications as JSON file. """ jsonlib.dump([f.to_json() for f in self], path, indent=4)
[docs] @classmethod def load(cls, path): """ Load feature specifications from a JSON file (e.g. as created with `FeatureCollection.dump`) """ return cls([Feature(**f) for f in jsonlib.load(path)])
def __call__(self, feature, language): return self[feature](language)