def format_key(key):
    if isinstance(key, list):
        for k in key:
            assert isinstance(k, str), "keys should be strings"
    else:
        assert isinstance(key, str)
    return key


class TransformByKey(object):
    """Performs data[outkey[0]], data[outkey[1]], ... = transform(data[key[0]], data[key[1]], ..)"""

    def __init__(self, transform, key=None, outkey=None, ignore_key_error=False, **kwargs):
        self.transform = transform
        if key is None:
            self.key = None
        else:
            self.key = format_key(key)
        if outkey is None:
            self.outkey = self.key
        else:
            self.outkey = format_key(outkey)
        self.ignore_key_error = ignore_key_error
        self.kwargs = kwargs

    def __call__(self, data):
        assert isinstance(data, dict), "Input data should be a dictionary, not a {}".format(type(data))
        try:
            if self.key is None:
                output = self.transform(**self.kwargs)
            elif isinstance(self.key, str):
                output = self.transform(data[self.key], **self.kwargs)
            else:
                inputs = [data[k] for k in self.key]
                output = self.transform(*inputs, **self.kwargs)

            if isinstance(self.outkey, str):
                data[self.outkey] = output
            else:
                assert isinstance(output, tuple), "Output should be tuple, not {} because outkey is {}".format(type(output), type(self.outkey))
                assert len(self.outkey) == len(output), "len(outkey) and len(output) should be the same for a 1-to-1 matching."
                for k, o in zip(self.outkey, output):
                    data[k] = o
        except KeyError as e:
            if not self.ignore_key_error:
                raise e
        return data
