def run(self):
        """
        Runs the task.

        This fires the ``on_start`` hook function first (if present), passing
        the task itself.

        Then it runs the target function supplied via ``Task.to_call`` with
        its arguments & stores the result.

        If the target function succeeded, the ``on_success`` hook function is
        called, passing both the task & the result to it.

        If the target function failed (threw an exception), the ``on_error``
        hook function is called, passing both the task & the exception to it.
        Then the exception is re-raised.

        Finally, the result is returned.
        """
        if self.on_start:
            self.on_start(self)

        try:
            result = self.func(*self.func_args, **self.func_kwargs)
        except Exception as err:
            self.to_failed()

            if self.on_error:
                self.on_error(self, err)

            raise

        self.to_success()

        if self.on_success:
            self.on_success(self, result)

        return result