r"""
    Wraps a function with a Cacher object

    uses a hash of arguments as input

    Args:
        fname (str):  file name (defaults to function name)
        cache_dir (unicode): (default = u'default')
        appname (unicode): (default = u'utool')
        key_argx (None): (default = None)
        key_kwds (None): (default = None)
        use_cache (bool):  turns on disk based caching(default = None)

    CommandLine:
        python -m utool.util_cache --exec-cached_func

    Example:
        >>> # ENABLE_DOCTEST
        >>> import utool as ut
        >>> def costly_func(a, b, c='d', *args, **kwargs):
        ...     return ([a] * b, c, args, kwargs)
        >>> ans0 = costly_func(41, 3)
        >>> ans1 = costly_func(42, 3)
        >>> closure_ = ut.cached_func('costly_func', appname='utool_test',
        >>>                           key_argx=[0, 1])
        >>> efficient_func = closure_(costly_func)
        >>> ans2 = efficient_func(42, 3)
        >>> ans3 = efficient_func(42, 3)
        >>> ans4 = efficient_func(41, 3)
        >>> ans5 = efficient_func(41, 3)
        >>> assert ans1 == ans2
        >>> assert ans2 == ans3
        >>> assert ans5 == ans4
        >>> assert ans5 == ans0
        >>> assert ans1 != ans0

def cached_func(fname=None, cache_dir='default', appname='utool', key_argx=None,
                key_kwds=None, use_cache=None, verbose=None):
    r"""
    Wraps a function with a Cacher object

    uses a hash of arguments as input

    Args:
        fname (str):  file name (defaults to function name)
        cache_dir (unicode): (default = u'default')
        appname (unicode): (default = u'utool')
        key_argx (None): (default = None)
        key_kwds (None): (default = None)
        use_cache (bool):  turns on disk based caching(default = None)

    CommandLine:
        python -m utool.util_cache --exec-cached_func

    Example:
        >>> # ENABLE_DOCTEST
        >>> import utool as ut
        >>> def costly_func(a, b, c='d', *args, **kwargs):
        ...     return ([a] * b, c, args, kwargs)
        >>> ans0 = costly_func(41, 3)
        >>> ans1 = costly_func(42, 3)
        >>> closure_ = ut.cached_func('costly_func', appname='utool_test',
        >>>                           key_argx=[0, 1])
        >>> efficient_func = closure_(costly_func)
        >>> ans2 = efficient_func(42, 3)
        >>> ans3 = efficient_func(42, 3)
        >>> ans4 = efficient_func(41, 3)
        >>> ans5 = efficient_func(41, 3)
        >>> assert ans1 == ans2
        >>> assert ans2 == ans3
        >>> assert ans5 == ans4
        >>> assert ans5 == ans0
        >>> assert ans1 != ans0
    """
    if verbose is None:
        verbose = VERBOSE_CACHE
    def cached_closure(func):
        from utool import util_decor
        import utool as ut
        fname_ = util_inspect.get_funcname(func) if fname is None else fname
        kwdefaults = util_inspect.get_kwdefaults(func)
        argnames   = util_inspect.get_argnames(func)
        if ut.is_method(func):
            # ignore self for methods
            argnames = argnames[1:]
        cacher = Cacher(fname_, cache_dir=cache_dir, appname=appname,
                        verbose=verbose)
        if use_cache is None:
            use_cache_ = not util_arg.get_argflag('--nocache-' + fname_)
        else:
            use_cache_ = use_cache
        #_dbgdict = dict(fname_=fname_, key_kwds=key_kwds, appname=appname,
        #                key_argx=key_argx, use_cache_=use_cache_)
        #@functools.wraps(func)
        def cached_wraper(*args, **kwargs):
            """
            Cached Wrapper Function

            Additional Kwargs:
                use_cache (bool) : enables cache
            """
            try:
                if verbose > 2:
                    print('[util_cache] computing cached function fname_=%s' %
                          ( fname_,))
                # Implicitly adds use_cache to kwargs
                cfgstr = get_cfgstr_from_args(func, args, kwargs, key_argx,
                                              key_kwds, kwdefaults, argnames)
                if util_cplat.WIN32:
                    # remove potentially invalid chars
                    cfgstr = '_' + util_hash.hashstr27(cfgstr)
                assert cfgstr is not None, 'cfgstr=%r cannot be None' % (cfgstr,)
                use_cache__ = kwargs.pop('use_cache', use_cache_)
                if use_cache__:
                    # Make cfgstr from specified input
                    data = cacher.tryload(cfgstr)
                    if data is not None:
                        return data
                # Cached missed compute function
                data = func(*args, **kwargs)
                # Cache save
                #if use_cache__:
                # TODO: save_cache
                cacher.save(data, cfgstr)
                return data
            #except ValueError as ex:
            # handle protocal error
            except Exception as ex:
                from utool import util_dbg
                _dbgdict2 = dict(key_argx=key_argx, lenargs=len(args),
                                 lenkw=len(kwargs),)
                msg = '\n'.join([
                    '+--- UTOOL --- ERROR IN CACHED FUNCTION',
                    #'dbgdict = ' + utool.repr4(_dbgdict),
                    'dbgdict2 = ' + util_str.repr4(_dbgdict2),
                ])
                util_dbg.printex(ex, msg)
                raise
        # Give function a handle to the cacher object
        cached_wraper = util_decor.preserve_sig(cached_wraper, func)
        cached_wraper.cacher = cacher
        return cached_wraper
    return cached_closure