Try to get the current attribute/function result for the given value.

        If ``args`` or ``kwargs`` are passed, the attribute got from ``value`` must be callable.
        They will be passed anyway (using ``[]`` and ``{}`` as default values) if the attribute
        is a function.

        And if it's not a function but a real attribute, if no args are passed but the attribute
        got from ``value`` is "callable", we'll try to call it without argument.
        If any exception occurs during the call, two things may happen:
        - the "thing" called is a function or method, then we raise the exception
        - it's something else, like an instance of a class having a ``__call__`` method,
          in this case we want to return the instance (if the user want the thing to be
          called it could still use parentheses)

        Arguments
        ---------
        value : ?
            The value from which we want the attribute.
        args : list, default ``None``
            If defined, list of non-named arguments that will be passed to the attribute if it's
            callable.
        kwargs : dict, default ``None``
            If defined, list of named arguments that will be passed to the attribute if it's
            callable.

        Returns
        -------
        Value of the current attribute/function result for the ``value`` instance.

        Raises
        ------
        dataql.solvers.exceptions.CallableError
            If an exception was raised when the attribute/function was called.

        Example
        -------

        >>> from datetime import date
        >>> d = date(2015, 6, 1)
        >>> Attribute('day').solve(d)
        1
        >>> Attribute('strftime').solve(d, ['%F'])
        '2015-06-01'
        >>> Attribute('day').solve('a string') # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql...AttributeNotFound: `<Attribute 'day'>` is not an allowed attribute
        >>> Attribute('day').solve(d, ['foo'])
        Traceback (most recent call last):
        TypeError: 'int' object is not callable
        >>> Attribute('str', function=str).solve(d)
        '2015-06-01'

        # Test the dict-like approach
        >>> class MyDict(dict):
        ...     foo = 1
        >>> d = MyDict(bar=2)
        >>> Attribute('foo').solve(d)
        1
        >>> Attribute('bar').solve(d)
        2

        Example with callable
        ----------------------

        >>> class Klass1:
        ...     def __call__(self):
        ...         return 'foo'
        >>> class Klass2:
        ...     def __call__(self, arg):
        ...         return 'foo'
        >>> class Klass3:
        ...     def __call__(self):
        ...         raise TypeError('error')
        >>> class Klass4:
        ...     def __call__(self, **kwargs):
        ...         return 'foo'
        >>> class Klass5:
        ...     def __call__(self, **kwargs):
        ...         return kwargs['foo']
        >>> class Klass6:
        ...     def __call__(self, *args):
        ...         return 'foo'
        >>> class Klass7:
        ...     def __call__(self, *args):
        ...         return args[0]
        >>> class Klass:
        ...     def call1(self):
        ...         return 'foo'
        ...     def call2(self, arg):
        ...         return 'foo'
        ...     def call3(self):
        ...         raise TypeError('error')
        ...     def call4(self, **kwargs):
        ...         return 'foo'
        ...     def call5(self, **kwargs):
        ...         return kwargs['foo']
        ...     def call6(self, *args):
        ...         return 'foo'
        ...     def call7(self, *args):
        ...         return args[0]
        ...     k1, k2, k3, k4 = Klass1(), Klass2(), Klass3(), Klass4()
        ...     k5, k6, k7 = Klass5(), Klass6(), Klass7()

        >>> v = Klass()

        # Can be called without arguments: we call it
        >>> Attribute('call1').solve(v)
        'foo'

        # TypeError while calling without argument, but it's a method: raise
        >>> Attribute('call2').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call2` (without arguments)

        # TypeError but no argument needed, but it's a method: raise
        >>> Attribute('call3').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call3` (without arguments)

        # Can be called without used kwargs: we call it
        >>> Attribute('call4').solve(v)
        'foo'

        # KeyError while calling without argument, in a get of kwargs, but it's a method: raise
        >>> Attribute('call5').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call5` (without arguments)

        # Can be called without used args: we call it
        >>> Attribute('call6').solve(v)
        'foo'

        # IndexError while calling without argument, in a get of args, but it's a method: raise
        >>> Attribute('call7').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call7` (without arguments)



        # Can be called without arguments: we call it
        >>> Attribute('k1').solve(v)
        'foo'

        # TypeError while calling without argument, but it's NOT a method: return not called
        >>> isinstance(Attribute('k2').solve(v), Klass2)
        True

        # TypeError but no argument needed, but it's NOT a method: return not called
        >>> isinstance(Attribute('k3').solve(v), Klass3)
        True

        # Can be called without used kwargs: we call it
        >>> Attribute('k4').solve(v)
        'foo'

        # KeyError while calling without argument, in a get of kwargs, but it's NOT a method:
        # return not called
        >>> isinstance(Attribute('k5').solve(v), Klass5)
        True

        # Can be called without used args: we call it
        >>> Attribute('k6').solve(v)
        'foo'

        # IndexError while calling without argument, in a get of args, but it's NOT a method:
        # return not called
        >>> isinstance(Attribute('k7').solve(v), Klass7)
        True

def solve(self, value, args=None, kwargs=None):
        """Try to get the current attribute/function result for the given value.

        If ``args`` or ``kwargs`` are passed, the attribute got from ``value`` must be callable.
        They will be passed anyway (using ``[]`` and ``{}`` as default values) if the attribute
        is a function.

        And if it's not a function but a real attribute, if no args are passed but the attribute
        got from ``value`` is "callable", we'll try to call it without argument.
        If any exception occurs during the call, two things may happen:
        - the "thing" called is a function or method, then we raise the exception
        - it's something else, like an instance of a class having a ``__call__`` method,
          in this case we want to return the instance (if the user want the thing to be
          called it could still use parentheses)

        Arguments
        ---------
        value : ?
            The value from which we want the attribute.
        args : list, default ``None``
            If defined, list of non-named arguments that will be passed to the attribute if it's
            callable.
        kwargs : dict, default ``None``
            If defined, list of named arguments that will be passed to the attribute if it's
            callable.

        Returns
        -------
        Value of the current attribute/function result for the ``value`` instance.

        Raises
        ------
        dataql.solvers.exceptions.CallableError
            If an exception was raised when the attribute/function was called.

        Example
        -------

        >>> from datetime import date
        >>> d = date(2015, 6, 1)
        >>> Attribute('day').solve(d)
        1
        >>> Attribute('strftime').solve(d, ['%F'])
        '2015-06-01'
        >>> Attribute('day').solve('a string') # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql...AttributeNotFound: `<Attribute 'day'>` is not an allowed attribute
        >>> Attribute('day').solve(d, ['foo'])
        Traceback (most recent call last):
        TypeError: 'int' object is not callable
        >>> Attribute('str', function=str).solve(d)
        '2015-06-01'

        # Test the dict-like approach
        >>> class MyDict(dict):
        ...     foo = 1
        >>> d = MyDict(bar=2)
        >>> Attribute('foo').solve(d)
        1
        >>> Attribute('bar').solve(d)
        2

        Example with callable
        ----------------------

        >>> class Klass1:
        ...     def __call__(self):
        ...         return 'foo'
        >>> class Klass2:
        ...     def __call__(self, arg):
        ...         return 'foo'
        >>> class Klass3:
        ...     def __call__(self):
        ...         raise TypeError('error')
        >>> class Klass4:
        ...     def __call__(self, **kwargs):
        ...         return 'foo'
        >>> class Klass5:
        ...     def __call__(self, **kwargs):
        ...         return kwargs['foo']
        >>> class Klass6:
        ...     def __call__(self, *args):
        ...         return 'foo'
        >>> class Klass7:
        ...     def __call__(self, *args):
        ...         return args[0]
        >>> class Klass:
        ...     def call1(self):
        ...         return 'foo'
        ...     def call2(self, arg):
        ...         return 'foo'
        ...     def call3(self):
        ...         raise TypeError('error')
        ...     def call4(self, **kwargs):
        ...         return 'foo'
        ...     def call5(self, **kwargs):
        ...         return kwargs['foo']
        ...     def call6(self, *args):
        ...         return 'foo'
        ...     def call7(self, *args):
        ...         return args[0]
        ...     k1, k2, k3, k4 = Klass1(), Klass2(), Klass3(), Klass4()
        ...     k5, k6, k7 = Klass5(), Klass6(), Klass7()

        >>> v = Klass()

        # Can be called without arguments: we call it
        >>> Attribute('call1').solve(v)
        'foo'

        # TypeError while calling without argument, but it's a method: raise
        >>> Attribute('call2').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call2` (without arguments)

        # TypeError but no argument needed, but it's a method: raise
        >>> Attribute('call3').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call3` (without arguments)

        # Can be called without used kwargs: we call it
        >>> Attribute('call4').solve(v)
        'foo'

        # KeyError while calling without argument, in a get of kwargs, but it's a method: raise
        >>> Attribute('call5').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call5` (without arguments)

        # Can be called without used args: we call it
        >>> Attribute('call6').solve(v)
        'foo'

        # IndexError while calling without argument, in a get of args, but it's a method: raise
        >>> Attribute('call7').solve(v) # doctest: +ELLIPSIS
        Traceback (most recent call last):
        dataql.solvers.exceptions.CallableError:...calling `call7` (without arguments)



        # Can be called without arguments: we call it
        >>> Attribute('k1').solve(v)
        'foo'

        # TypeError while calling without argument, but it's NOT a method: return not called
        >>> isinstance(Attribute('k2').solve(v), Klass2)
        True

        # TypeError but no argument needed, but it's NOT a method: return not called
        >>> isinstance(Attribute('k3').solve(v), Klass3)
        True

        # Can be called without used kwargs: we call it
        >>> Attribute('k4').solve(v)
        'foo'

        # KeyError while calling without argument, in a get of kwargs, but it's NOT a method:
        # return not called
        >>> isinstance(Attribute('k5').solve(v), Klass5)
        True

        # Can be called without used args: we call it
        >>> Attribute('k6').solve(v)
        'foo'

        # IndexError while calling without argument, in a get of args, but it's NOT a method:
        # return not called
        >>> isinstance(Attribute('k7').solve(v), Klass7)
        True

        """

        # If we have a function, apply this, using the value as first argument,
        # then args and kwargs.
        if self.function:
            try:
                return self.function(value, *(args or []), **(kwargs or {}))
            except Exception as ex:
                raise CallableError(self, value, args, kwargs, ex)

        # Manage a normal attribute.
        try:
            # try to get an attribute.
            result = getattr(value, self.name)
        except AttributeError:
            try:
                # Attribute not found, try to get a key.
                result = value[self.name]
            except (TypeError, KeyError):
                # No attribute, no key.
                raise AttributeNotFound(self)

        # We make a call from the attribute in all cases if we have some arguments.
        if args is not None or kwargs is not None:
            result = result(*(args or []), **(kwargs or {}))

        elif not isclass(result) and callable(result):
            # We have no arguments, but the attribute is a callable, so, if it not a class,
            # we'll try to call it, without arguments.
            try:
                result = result()

            except Exception as ex:
                # If an exception is raised during the call, we have two cases:
                # - the "thing" called is a function or method, then we re-raise the exception
                # - it's something else, like an instance of a class having a ``__call__`` method,
                #   in this case we want to return the instance (if the user want the thing to be
                #   called it could still use parentheses)
                if isfunction(result) or ismethod(result):
                    raise CallableError(self, value, args, kwargs, ex)

        return result