import inspect
import functools


def loggedmethod(method):
    """Decorator for LoggedObject methods, ensuring exceptions logging.

    Whenever an exception is raised by a method this function decorates,
    its details are logged through the object's `log` method at 'error'
    level before it is raised again.

    This is useful when unexpected exceptions may be raised in a context
    where they will not interrupt execution but need to be notified.

    Usage:
    >>> class Foo(LoggedObject):
    ...     @loggedmethod
    ...     def bar(self, x):
    ...         if not isinstance(x, str);
    ...             raise TypeError('Expected "x" to be a str.')
    ...         self.log(x)
    ...
    >>> foo = Foo()
    >>> foo.bar('Some string.')
    <asctime> : Some string.
    >>> foo.bar(42)
    <asctime> : TypeError at `Foo.bar`: Expected "x" to be a str.
    TypeError: Expected "x" to be a str.
    """
    if 'self' not in inspect.signature(method).parameters.keys():
        raise RuntimeError(
            "Attempt at decorating a function with no 'self' argument "
            + "using '@logged_method'."
        )
    @functools.wraps(method)
    def logged_method(self, *args, **kwargs):
        """Wrapped method ensuring exceptions logging before raising."""
        try:
            return method(self, *args, **kwargs)
        except Exception as exception:
            method_name = getattr(method, '__qualname__', method.__name__)
            msg = "%s at '%s': %s" % (
                type(exception).__name__, method_name,
                ';'.join(map(str, exception.args))
            )
            self.log(msg=msg, level='error')
            raise exception
    return logged_method