'''
Some tools for aggregating error messages.

These solve the problem where you wish to perform a number of subtasks,
continuing if any of the fail, and you want to aggregate all the errors
generated while doing this.

One use case for this is in form validation with human-readable output. A
simple, pythonic validation function would iterate over the fields in the form,
and would raise an exception as soon as it found any field that wasn't valid.
But when the errors are displayed to the user, you will likely want to show ALL
the errors on the form, not just the first one found.

So instead, you want to validate each field of the form regardless of whether or
not the previous fields were valid, and collect a list of all the errors that
came up, so that you can show them all to the user.

If the form has three fields called 'foo', 'bar', and 'baz', then you would want
to generate an exception with three sub-entries - one for each of these fields -
plus a list of any errors that are not specific to one field, such as violating
a constraint involving two fields' values.

The ErrorAggregator class provides tools for doing this, and the NestedException
class defines an Exception subclass well-suited to storing these errors.

'''

from contextlib import contextmanager


class NestedException(Exception):
    '''Exception with sub-exceptions.

    A NestedException has two useful fields, own_errors and sub_errors.

    own_errors is a list of root errors raised during some operation.

    sub_errors is a mapping from strings to NestedExceptions, representing the
    exceptions generated by named sub-steps in the operation.
    '''
    def __init__(self):
        self.own_errors = []
        self.sub_errors = {}
        super(NestedException, self).__init__()

    def __nonzero__(self):
        return bool(self.own_errors) or bool(self.sub_errors)

    def __repr__(self):
        pieces = [self.own_errors, self.sub_errors]
        return ', '.join(sorted(repr(x) for x in pieces if x))

    def __getitem__(self, item):
        if isinstance(item, int):
            return self.own_errors[item]
        return self.sub_errors[item]

    def add_own(self, exc):
        if isinstance(exc, NestedException):
            self.merge(exc)
        else:
            self.own_errors.append(exc)

    def add_sub(self, key, exc):
        '''Add a sub-exception.'''
        self.sub_errors.setdefault(key, type(self)()).add_own(exc)

    def merge(self, other):
        '''Merge another NestedException into this one.'''
        for own in other.own_errors:
            self.add_own(own)
        for key, sub in other.sub_errors.items():
            self.add_sub(key, sub)

    def __str__(self):
        own = ', '.join(str(err) for err in self.own_errors)
        other = ', '.join('{0}: [{1}]'.format(key, err) for (key, err) in self.sub_errors.items())
        return ' '.join(x for x in [own, other] if x)


class ErrorAggregator(object):
    '''Helper class for trying several things and accumulating all errors.

    Basic usage:
    >>> eg = ErrorAggregator()
    >>> eg.own_error(Exception("something_wrong"))
    >>> with eg.checking_sub("x"):
    ...     raise Exception("x_failed")
    >>> eg.has_errors()
    True
    >>> eg.error.own_errors
    [Exception('something_wrong',)]
    >>> eg.error.sub_errors
    {'x': [Exception('x_failed',)]}

    If autoraise is True, then instead of aggregating it will raise an error as
    soon as one is added:

    >>> eg = ErrorAggregator(autoraise=True)
    >>> eg.own_error(Exception("something_wrong"))
    Traceback (most recent call last):
        ...
    NestedException: something_wrong
    '''
    error_type = NestedException
    catch_type = Exception

    def __init__(self, autoraise=False):
        self.error = self.error_type()
        self.autoraise = autoraise

    def own_error(self, err):
        '''Add a self-error.'''
        self.error.add_own(err)
        if self.autoraise:
            raise self.error

    def sub_error(self, key, err):
        '''Add a child-error.'''
        self.error.add_sub(key, err)
        if self.autoraise:
            raise self.error

    @contextmanager
    def checking(self):
        '''Context manager for try-excepting tasks.'''
        try:
            yield
        except self.catch_type, e:
            self.own_error(e)

    @contextmanager
    def checking_sub(self, key):
        '''Context manager for try-excepting subtasks.'''
        try:
            yield
        except self.catch_type, e:
            self.sub_error(key, e)

    def has_errors(self):
        '''Returns true if the aggregator holds any errors.'''
        return bool(self.error)

    def raise_if_any(self):
        '''If the aggregator has any exceptions, raise a NestedException'''
        if self.has_errors():
            raise self.error


if __name__ == '__main__':
    import doctest
    doctest.testmod()
