Source code for datawork.api.tool

"""Module implementing Tool and Invocation."""

import hashlib
from abc import abstractstaticmethod

from .cache import Hashable
from .config import Configurable, Option
from .data import Data
from .invocation import Invocation


[docs]class Tool(Configurable, Hashable): """ A class for composable tools. This is the base class for configurable functions that transform :class:`~.data.Data` objects. """ # Each of these should be lists of placeholder objects that subclass 'Data' INPUTS = [] OUTPUTS = []
[docs] def __init__(self): """Construct an object with a new configuration.""" Configurable.__init__(self) Hashable.__init__(self)
[docs] def __repr__(self): """Return the name of the tool and its config.""" return self.__class__.__name__ + ": config=" + repr(self.config)
[docs] def version(self): """ Return a version string for this tool. Subclasses implement their own version methods. """ raise NotImplementedError
[docs] def get_hash(self): """Return a hash string for this tool and configuration.""" return "" # TODO: add tool version identifier config_hash = self.config.get_hash() return hashlib.md5(("tool" + config_hash).encode("ascii")).hexdigest()
[docs] @abstractstaticmethod def run(cfg): """ Compute outputs. This method is the meat of the :class:`~.tool.Tool` class. """ pass
[docs] def __call__(self, *args): """ Let a tool act on some :class:`~.data.Data` objects. Calling a tool instance _invokes_ the tool, which results in an :class:`~.invocation.Invocation` instance if the arguments are of class :class:`~.data.Data`. """ newargs = [] for ii, a in zip(self.INPUTS, args): if a is None: # create a new placeholder object a = ii.__class__() assert isinstance( a, Data ), "__call__() expects all arguments to be instances of Data or None" newargs.append(a) return Invocation(self, tuple(newargs)).o
[docs]def tool(r): """ Quickly create a :class:`~.tool.Tool` class. Given a function definition for a tool, create a class with the provided function as the class's :meth:`~.tool.Tool.run` method. """ import inspect ins = [] outs = [] opts = [] sig = inspect.signature(r) for arg, p in sig.parameters.items(): ann = p.annotation if isinstance(ann, Data): ann.name = arg # monkey patch the name onto the annotation ins.append(ann) elif isinstance(ann, Option): ann.name = arg if p.default is p.empty: ann.default = p.default opts.append(ann) else: print(arg) raise Exception("All arguments must be annotated") # handle return annotation ann = sig.return_annotation if ann is inspect.Parameter.empty: raise Exception("Return values must be annotated") if not isinstance(ann, (list, tuple)): ann = [ann] for oi in ann: outs.append(oi) return type(r.__name__, (Tool,), dict(run=r, __doc__=r.__doc__, INPUTS=ins, OUTPUTS=outs, OPTIONS=opts))