# Source code for ubelt.util_indexable

    """
    The util_indexable module defines ``IndexableWalker`` which is a powerful
    way to iterate through nested Python containers.

    RelatedWork:
        * [PypiDictDigger]_

    References:
        .. [PypiDictDigger] https://pypi.org/project/dict_digger/
        .. [PypiDeepDiff] https://pypi.org/project/deepdiff/
    """
    from math import isclose
    from collections.abc import Generator
    from typing import NamedTuple, Tuple, Any
    # from collections.abc import Iterable

    try:
        from functools import cache
    except ImportError:  # nocover
        from ubelt.util_memoize import memoize as cache

    @cache
    def _lazy_numpy():
        try:
            import numpy as np
        except ImportError:  # nocover
            return None
        return np

    class Difference(NamedTuple):
        """
        A result class of indexable_diff that organizes what the difference between
        the indexables is.
        """
        path: Tuple
        value1: Any
        value2: Any

    class IndexableWalker(Generator):
        """
        Traverses through a nested tree-liked indexable structure.

        Generates a path and value to each node in the structure. The path is a
        list of indexes which if applied in order will reach the value.

        The ``__setitem__`` method can be used to modify a nested value based on the
        path returned by the generator.

        When generating values, you can use "send" to prevent traversal of a
        particular branch.

        RelatedWork:
            * https://pypi.org/project/python-benedict/ - implements a dictionary
                subclass with similar nested indexing abilities.

        Attributes:
            data (dict | list | tuple): the wrapped indexable data

            dict_cls (Tuple[type]):
                the types that should be considered dictionary mappings for the
                purpose of nested iteration. Defaults to ``dict``.

            list_cls (Tuple[type]):
                the types that should be considered list-like for the purposes
                of nested iteration. Defaults to ``(list, tuple)``.

            indexable_cls (Tuple[type]):
                combined dict_cls and list_cls

        Example:
            >>> import ubelt as ub
            >>> # Given Nested Data
            >>> data = {
            >>>     'foo': {'bar': 1},
            >>>     'baz': [{'biz': 3}, {'buz': [4, 5, 6]}],
            >>> }
            >>> # Create an IndexableWalker
            >>> walker = ub.IndexableWalker(data)
            >>> # We iterate over the data as if it was flat
            >>> # ignore the <want> string due to order issues on older Pythons
            >>> # xdoctest: +IGNORE_WANT
            >>> for path, val in walker:
            >>>     print(path)
            ['foo']
            ['baz']
            ['baz', 0]
            ['baz', 1]
            ['baz', 1, 'buz']
            ['baz', 1, 'buz', 0]
            ['baz', 1, 'buz', 1]
            ['baz', 1, 'buz', 2]
            ['baz', 0, 'biz']
            ['foo', 'bar']
            >>> # We can use "paths" as keys to getitem into the walker
            >>> path = ['baz', 1, 'buz', 2]
            >>> val = walker[path]
            >>> assert val == 6
            >>> # We can use "paths" as keys to setitem into the walker
            >>> assert data['baz'][1]['buz'][2] == 6
            >>> walker[path] = 7
            >>> assert data['baz'][1]['buz'][2] == 7
            >>> # We can use "paths" as keys to delitem into the walker
            >>> assert data['baz'][1]['buz'][1] == 5
            >>> del walker[['baz', 1, 'buz', 1]]
            >>> assert data['baz'][1]['buz'][1] == 7

        Example:
            >>> # Create nested data
            >>> # xdoctest: +REQUIRES(module:numpy)
            >>> import numpy as np
            >>> import ubelt as ub
            >>> data = ub.ddict(lambda: int)
            >>> data['foo'] = ub.ddict(lambda: int)
            >>> data['bar'] = np.array([1, 2, 3])
            >>> data['foo']['a'] = 1
            >>> data['foo']['b'] = np.array([1, 2, 3])
            >>> data['foo']['c'] = [1, 2, 3]
            >>> data['baz'] = 3
            >>> print('data = {}'.format(ub.repr2(data, nl=True)))
            >>> # We can walk through every node in the nested tree
            >>> walker = ub.IndexableWalker(data)
            >>> for path, value in walker:
            >>>     print('walk path = {}'.format(ub.repr2(path, nl=0)))
            >>>     if path[-1] == 'c':
            >>>         # Use send to prevent traversing this branch
            >>>         got = walker.send(False)
            >>>         # We can modify the value based on the returned path
            >>>         walker[path] = 'changed the value of c'
            >>> print('data = {}'.format(ub.repr2(data, nl=True)))
            >>> assert data['foo']['c'] == 'changed the value of c'

        Example:
            >>> # Test sending false for every data item
            >>> import ubelt as ub
            >>> data = {1: [1, 2, 3], 2: [1, 2, 3]}
            >>> walker = ub.IndexableWalker(data)
            >>> # Sending false means you wont traverse any further on that path
            >>> num_iters_v1 = 0
            >>> for path, value in walker:
            >>>     print('[v1] walk path = {}'.format(ub.repr2(path, nl=0)))
            >>>     walker.send(False)
            >>>     num_iters_v1 += 1
            >>> num_iters_v2 = 0
            >>> for path, value in walker:
            >>>     # When we dont send false we walk all the way down
            >>>     print('[v2] walk path = {}'.format(ub.repr2(path, nl=0)))
            >>>     num_iters_v2 += 1
            >>> assert num_iters_v1 == 2
            >>> assert num_iters_v2 == 8

        Example:
            >>> # Test numpy
            >>> # xdoctest: +REQUIRES(CPython)
            >>> # xdoctest: +REQUIRES(module:numpy)
            >>> import ubelt as ub
            >>> import numpy as np
            >>> # By default we don't recurse into ndarrays because they
            >>> # Are registered as an indexable class
            >>> data = {2: np.array([1, 2, 3])}
            >>> walker = ub.IndexableWalker(data)
            >>> num_iters = 0
            >>> for path, value in walker:
            >>>     print('walk path = {}'.format(ub.repr2(path, nl=0)))
            >>>     num_iters += 1
            >>> assert num_iters == 1
            >>> # Currently to use top-level ndarrays, you need to extend what the
            >>> # list class is. This API may change in the future to be easier
            >>> # to work with.
            >>> data = np.random.rand(3, 5)
            >>> walker = ub.IndexableWalker(data, list_cls=(list, tuple, np.ndarray))
            >>> num_iters = 0
            >>> for path, value in walker:
            >>>     print('walk path = {}'.format(ub.repr2(path, nl=0)))
            >>>     num_iters += 1
            >>> assert num_iters == 3 + 3 * 5
        """

        def __init__(self, data, dict_cls=(dict,), list_cls=(list, tuple)):
            self.data = data
            self.dict_cls = dict_cls
            self.list_cls = list_cls
            self.indexable_cls = self.dict_cls + self.list_cls
            self._walk_gen = None

        def __iter__(self):
            """
            Iterates through the indexable ``self.data``

            Can send a False flag to prevent a branch from being traversed

            Returns:
                Generator[Tuple[List, Any], Any, Any]:
                    path (List): list of index operations to arrive at the value
                    value (Any): the value at the path
            """
            # Calling iterate multiple times will clobber the internal state
            self._walk_gen = self._walk()
            return self._walk_gen

        def __next__(self):
            """
            returns next item from this generator

            Returns:
                Any
            """
            if self._walk_gen is None:
                self._walk_gen = self._walk()
            return next(self._walk_gen)

        # TODO: maybe we implement a map function?

        def send(self, arg):
            """
            send(arg) -> send 'arg' into generator,
            return next yielded value or raise StopIteration.
            """
            # Note: this will error if called before __next__
            self._walk_gen.send(arg)

        def throw(self, typ, val=None, tb=None):  # type: ignore
            """
            throw(typ[,val[,tb]]) -> raise exception in generator,
            return next yielded value or raise StopIteration.

            Args:
                typ (Any):
                    Type of the exception.
                    Should be a ``type[BaseException]``, type checking is not working right here.

                val (Optional[object]):

                tb (Optional[TracebackType]):

            Returns:
                Any

            Raises:
                StopIteration

            References:
                .. [GeneratorThrow] https://docs.python.org/3/reference/expressions.html#generator.throw
            """
            raise StopIteration

        def __setitem__(self, path, value):
            """
            Set nested value by path

            Args:
                path (List): list of indexes into the nested structure
                value (Any): new value
            """
            import itertools as it
            d = self.data
            # note: slice unpack seems faster in 3.9 at least, dont change
            # ~/misc/tests/python/bench_unpack.py
            # Using islice allows path to be a list or deque
            key_index = len(path) - 1
            prefix = it.islice(path, 0, key_index)
            key = path[key_index]
            # prefix, key = path[:-1], path[-1]
            # *prefix, key = path
            for k in prefix:
                d = d[k]
            d[key] = value

        def __getitem__(self, path):
            """
            Get nested value by path

            Args:
                path (List): list of indexes into the nested structure

            Returns:
                Any: value
            """
            import itertools as it
            d = self.data
            # Using islice allows path to be a list or deque
            key_index = len(path) - 1
            prefix = it.islice(path, 0, key_index)
            key = path[key_index]
            # prefix, key = path[:-1], path[-1]
            # *prefix, key = path
            for k in prefix:
                d = d[k]
            return d[key]

        def __delitem__(self, path):
            """
            Remove nested value by path

            Note:
                It can be dangerous to use this while iterating (because we may try
                to descend into a deleted location) or on leaf items that are
                list-like (because the indexes of all subsequent items will be
                modified).

            Args:
                path (List): list of indexes into the nested structure.
                    The item at the last index will be removed.
            """
            d = self.data
            prefix, key = path[:-1], path[-1]
            # *prefix, key = path
            for k in prefix:
                d = d[k]
            del d[key]

        def keys(self, non_leaf=False):
            """
            Iterate over the nested paths in the container.

            Args:
                non_leaf (bool):
                    if True, also returns non-leaf indexes, which are
                    indexes to intermediate nested structures.
                    Defaults to False.

            Yields:
                List: a path of indexes into the nested structure

            Example:
                >>> import ubelt as ub
                >>> data = {
                >>>     'foo': {'bar': 1},
                >>>     'baz': [{'biz': 3}, 'mid', {'buz': [4, 5, 6]}],
                >>>     'emptynest': [[]],
                >>> }
                >>> keys = list(ub.IndexableWalker(data).keys())
                >>> keys_with_nonleafs = list(ub.IndexableWalker(data).keys(non_leaf=True))
                >>> print(f'keys = {ub.urepr(keys, nl=1)}')
                keys = [
                    ['baz', 1],
                    ['baz', 2, 'buz', 0],
                    ['baz', 2, 'buz', 1],
                    ['baz', 2, 'buz', 2],
                    ['baz', 0, 'biz'],
                    ['foo', 'bar'],
                ]
                >>> assert not any(['emptynest' in p for p in keys])
                >>> assert any(['emptynest' in p for p in keys_with_nonleafs])
            """
            for path, value in self._walk():
                if non_leaf or not isinstance(value, self.indexable_cls):
                    yield path

        def values(self, non_leaf=False):
            """
            Iterate over the values nested within the container.

            Args:
                non_leaf (bool):
                    if True, also returns non-leaf indexes, which are
                    indexes to intermediate nested structures.
                    Defaults to False.

            Yields:
                Any: the value at each nested path.

            Example:
                >>> import ubelt as ub
                >>> data = {
                >>>     'foo': {'bar': 1},
                >>>     'baz': [{'biz': 3}, 'mid', {'buz': [4, 5, 6]}],
                >>>     'emptynest': [[]],
                >>> }
                >>> values = list(ub.IndexableWalker(data).values())
                >>> keys_with_nonleafs = list(ub.IndexableWalker(data).values(non_leaf=True))
                >>> print(values)
                ['mid', 4, 5, 6, 3, 1]
                >>> assert not any(isinstance(v, list) for v in values)
                >>> assert any(isinstance(v, list) for v in keys_with_nonleafs)
            """
            for path, value in self._walk():
                if non_leaf or not isinstance(value, self.indexable_cls):
                    yield value

        def _walk(self, data=None, prefix=[]):
            """
            Defines the underlying generator used by IndexableWalker

            Yields:
                Tuple[List, Any] | None:
                    path (List) - a "path" through the nested data structure
                    value (Any) - the value indexed by that "path".

                Can also yield None in the case that `send` is called on the
                generator.
            """
            if data is None:  # pragma: nobranch
                data = self.data
            stack = [(data, prefix)]
            while stack:
                _data, _prefix = stack.pop()
                # Create an items iterable of depending on the indexable data type
                if isinstance(_data, self.list_cls):
                    items = enumerate(_data)
                elif isinstance(_data, self.dict_cls):
                    items = _data.items()
                else:
                    raise TypeError(type(_data))

                for key, value in items:
                    # Yield the full path to this position and its value
                    path = _prefix + [key]
                    message = yield path, value
                    # If the value at this path is also indexable, then continue
                    # the traversal, unless the False message was explicitly sent
                    # by the caller.
                    if message is False:
                        # Because the `send` method will return the next value,
                        # we yield a dummy value so we don't clobber the next
                        # item in the traversal.
                        yield None
                    else:
                        if isinstance(value, self.indexable_cls):
                            stack.append((value, path))

        def allclose(self, other, rel_tol=1e-9, abs_tol=0.0, equal_nan=False, return_info=False):
            """
            Walks through this and another nested data structures and checks if
            everything is roughly the same.

            Args:
                other (IndexableWalker | List | Dict):
                    a nested indexable item to compare against.

                rel_tol (float):
                    maximum difference for being considered "close", relative to the
                    magnitude of the input values

                abs_tol (float):
                    maximum difference for being considered "close", regardless of the
                    magnitude of the input values

                equal_nan (bool):
                    if True, numpy must be available, and consider nans as equal.

                return_info (bool):
                    if True, return extra info dict. Defaults to False.

            Returns:
                bool | Tuple[bool, Dict] :
                    A boolean result if ``return_info`` is false, otherwise a tuple of
                    the boolean result and an "info" dict containing detailed results
                    indicating what matched and what did not.

            Example:
                >>> import ubelt as ub
                >>> items1 = ub.IndexableWalker({
                >>>     'foo': [1.222222, 1.333],
                >>>     'bar': 1,
                >>>     'baz': [],
                >>> })
                >>> items2 = ub.IndexableWalker({
                >>>     'foo': [1.22222, 1.333],
                >>>     'bar': 1,
                >>>     'baz': [],
                >>> })
                >>> flag, return_info =  items1.allclose(items2, return_info=True)
                >>> print('return_info = {}'.format(ub.repr2(return_info, nl=1)))
                >>> print('flag = {!r}'.format(flag))
                >>> for p1, v1, v2  in return_info['faillist']:
                >>>     v1_ = items1[p1]
                >>>     print('*fail p1, v1, v2 = {}, {}, {}'.format(p1, v1, v2))
                >>> for p1 in return_info['passlist']:
                >>>     v1_ = items1[p1]
                >>>     print('*pass p1, v1_ = {}, {}'.format(p1, v1_))
                >>> assert not flag

                >>> import ubelt as ub
                >>> items1 = ub.IndexableWalker({
                >>>     'foo': [1.0000000000000000000000001, 1.],
                >>>     'bar': 1,
                >>>     'baz': [],
                >>> })
                >>> items2 = ub.IndexableWalker({
                >>>     'foo': [0.9999999999999999, 1.],
                >>>     'bar': 1,
                >>>     'baz': [],
                >>> })
                >>> flag, return_info =  items1.allclose(items2, return_info=True)
                >>> print('return_info = {}'.format(ub.repr2(return_info, nl=1)))
                >>> print('flag = {!r}'.format(flag))
                >>> assert flag

            Example:
                >>> import ubelt as ub
                >>> flag, return_info =  ub.IndexableWalker([]).allclose(ub.IndexableWalker([]), return_info=True)
                >>> print('return_info = {!r}'.format(return_info))
                >>> print('flag = {!r}'.format(flag))
                >>> assert flag

            Example:
                >>> import ubelt as ub
                >>> flag =  ub.IndexableWalker([]).allclose([], return_info=False)
                >>> print('flag = {!r}'.format(flag))
                >>> assert flag

            Example:
                >>> import ubelt as ub
                >>> flag, return_info =  ub.IndexableWalker([]).allclose([1], return_info=True)
                >>> print('return_info = {!r}'.format(return_info))
                >>> print('flag = {!r}'.format(flag))
                >>> assert not flag

            Example:
                >>> # xdoctest: +REQUIRES(module:numpy)
                >>> import ubelt as ub
                >>> import numpy as np
                >>> a = np.random.rand(3, 5)
                >>> b = a + 1
                >>> wa = ub.IndexableWalker(a, list_cls=(np.ndarray,))
                >>> wb = ub.IndexableWalker(b, list_cls=(np.ndarray,))
                >>> flag, return_info =  wa.allclose(wb, return_info=True)
                >>> print('return_info = {!r}'.format(return_info))
                >>> print('flag = {!r}'.format(flag))
                >>> assert not flag
                >>> a = np.random.rand(3, 5)
                >>> b = a.copy() + 1e-17
                >>> wa = ub.IndexableWalker([a], list_cls=(np.ndarray, list))
                >>> wb = ub.IndexableWalker([b], list_cls=(np.ndarray, list))
                >>> flag, return_info =  wa.allclose(wb, return_info=True)
                >>> assert flag
                >>> print('return_info = {!r}'.format(return_info))
                >>> print('flag = {!r}'.format(flag))
            """
            walker1 = self
            if isinstance(other, IndexableWalker):
                walker2 = other
            else:
                walker2 = IndexableWalker(other, dict_cls=self.dict_cls,
                                          list_cls=self.list_cls)

            _isclose_fn, _iskw = _make_isclose_fn(rel_tol, abs_tol, equal_nan)

            flat_items1 = [
                (path, value) for path, value in walker1
                if not isinstance(value, walker1.indexable_cls) or len(value) == 0]
            flat_items2 = [
                (path, value) for path, value in walker2
                if not isinstance(value, walker1.indexable_cls) or len(value) == 0]

            flat_items1 = sorted(flat_items1)
            flat_items2 = sorted(flat_items2)

            if len(flat_items1) != len(flat_items2):
                info = {
                    'faillist': ['length mismatch']
                }
                final_flag = False
            else:
                passlist = []
                faillist = []

                for t1, t2 in zip(flat_items1, flat_items2):
                    p1, v1 = t1
                    p2, v2 = t2
                    assert p1 == p2, 'paths to the nested items should be the same'

                    # TODO: Could add a numpy optimization here.

                    flag = (v1 == v2) or (
                        isinstance(v1, float) and isinstance(v2, float) and
                        _isclose_fn(v1, v2, **_iskw)
                    )
                    if flag:
                        passlist.append(p1)
                    else:
                        faillist.append((p1, v1, v2))

                final_flag = len(faillist) == 0
                info = {
                    'passlist': passlist,
                    'faillist': faillist,
                }

            if return_info:
                info.update({
                    'walker1': walker1,
                    'walker2': walker2,
                })
                return final_flag, info
            else:
                return final_flag

        def diff(self, other, rel_tol=1e-9, abs_tol=0.0, equal_nan=False):
            """
            Walks through two nested data structures finds differences in the
            structures.

            Args:
                other (IndexableWalker | List | Dict):
                    a nested indexable item to compare against.

                rel_tol (float):
                    maximum difference for being considered "close", relative to the
                    magnitude of the input values

                abs_tol (float):
                    maximum difference for being considered "close", regardless of the
                    magnitude of the input values

                equal_nan (bool):
                    if True, numpy must be available, and consider nans as equal.

            Returns:
                dict: information about the diff with
                    "similarity": a score between 0 and 1
                    "num_differences" being the number of paths not common plus the
                        number of common paths with differing values.
                    "unique1": being the paths that were unique to self
                    "unique2": being the paths that were unique to other
                    "faillist": a list 3-tuples of common path and differing values
                    "num_approximations":
                        is the number of approximately equal items (i.e. floats) there were

            Example:
                >>> import ubelt as ub
                >>> dct1 = {
                >>>     'foo': [1.222222, 1.333],
                >>>     'bar': 1,
                >>>     'baz': [],
                >>>     'top': [1, 2, 3],
                >>>     'L0': {'L1': {'L2': {'K1': 'V1', 'K2': 'V2', 'D1': 1, 'D2': 2}}},
                >>> }
                >>> dct2 = {
                >>>     'foo': [1.22222, 1.333],
                >>>     'bar': 1,
                >>>     'baz': [],
                >>>     'buz': {1: 2},
                >>>     'top': [1, 1, 2],
                >>>     'L0': {'L1': {'L2': {'K1': 'V1', 'K2': 'V2', 'D1': 10, 'D2': 20}}},
                >>> }
                >>> info = ub.IndexableWalker(dct1).diff(dct2)
                >>> print(f'info = {ub.urepr(info, nl=2)}')

            Example:
                >>> # xdoctest: +REQUIRES(module:numpy)
                >>> import ubelt as ub
                >>> import numpy as np
                >>> a = np.random.rand(3, 5)
                >>> b = a + 1
                >>> wa = ub.IndexableWalker(a, list_cls=(np.ndarray,))
                >>> wb = ub.IndexableWalker(b, list_cls=(np.ndarray,))
                >>> info =  wa.diff(wb)
                >>> print(f'info = {ub.urepr(info, nl=2)}')
                >>> a = np.random.rand(3, 5)
                >>> b = a.copy() + 1e-17
                >>> wa = ub.IndexableWalker([a], list_cls=(np.ndarray, list))
                >>> wb = ub.IndexableWalker([b], list_cls=(np.ndarray, list))
                >>> info =  wa.diff(wb)
                >>> print(f'info = {ub.urepr(info, nl=2)}')

            Example:
                >>> import ubelt as ub
                >>> # test null similarity
                >>> wa = ub.IndexableWalker({}).diff({})
                >>> assert wa['similarity'] == 1.0
            """
            walker1 = self
            if isinstance(other, IndexableWalker):
                walker2 = other
            else:
                walker2 = IndexableWalker(other, dict_cls=self.dict_cls,
                                          list_cls=self.list_cls)
            # TODO: numpy optimizations
            flat_items1 = {
                tuple(path): value for path, value in walker1
                if not isinstance(value, walker1.indexable_cls) or len(value) == 0}
            flat_items2 = {
                tuple(path): value for path, value in walker2
                if not isinstance(value, walker1.indexable_cls) or len(value) == 0}

            common = flat_items1.keys() & flat_items2.keys()
            unique1 = flat_items1.keys() - flat_items2.keys()
            unique2 = flat_items2.keys() - flat_items1.keys()

            num_approximations = 0

            _isclose_fn, _iskw = _make_isclose_fn(rel_tol, abs_tol, equal_nan)

            faillist = []
            passlist = []
            for key in common:
                v1 = flat_items1[key]
                v2 = flat_items2[key]
                flag = (v1 == v2)
                if not flag:
                    flag = (
                        isinstance(v1, float) and isinstance(v2, float) and
                        _isclose_fn(v1, v2, **_iskw)
                    )
                    num_approximations += flag
                if flag:
                    passlist.append(key)
                else:
                    faillist.append(Difference(key, v1, v2))

            num_differences = len(unique1) + len(unique2) + len(faillist)
            num_similarities = len(passlist)

            if num_similarities == 0 and num_differences == 0:
                similarity = 1.0
            else:
                similarity = num_similarities / (num_similarities + num_differences)
            info = {
                'similarity': similarity,
                'num_approximations': num_approximations,
                'num_differences': num_differences,
                'num_similarities': num_similarities,
                'unique1': unique1,
                'unique2': unique2,
                'faillist': faillist,
                'passlist': passlist,
            }
            return info

    def _make_isclose_fn(rel_tol, abs_tol, equal_nan):
        np = _lazy_numpy()
        if np is None:  # nocover
            _isclose_fn = isclose
            _iskw = dict(rel_tol=rel_tol, abs_tol=abs_tol)
            if equal_nan:
                raise NotImplementedError('requires numpy')
        else:
            _isclose_fn = np.isclose
            _iskw = dict(rtol=rel_tol, atol=abs_tol, equal_nan=equal_nan)
        return _isclose_fn, _iskw

    def indexable_allclose(items1, items2, rel_tol=1e-9, abs_tol=0.0, return_info=False):
        """
        Walks through two nested data structures and ensures that everything is
        roughly the same.

        NOTE:
            Deprecated. Instead use:
                ub.IndexableWalker(items1).allclose(items2)

        Args:
            items1 (dict | list | tuple):
                a nested indexable item

            items2 (dict | list | tuple):
                a nested indexable item

            rel_tol (float):
                maximum difference for being considered "close", relative to the
                magnitude of the input values

            abs_tol (float):
                maximum difference for being considered "close", regardless of the
                magnitude of the input values

            return_info (bool): if True, return extra info. Defaults to False.

        Returns:
            bool | Tuple[bool, Dict] :
                A boolean result if ``return_info`` is false, otherwise a tuple of
                the boolean result and an "info" dict containing detailed results
                indicating what matched and what did not.

        Example:
            >>> import ubelt as ub
            >>> items1 = {
            >>>     'foo': [1.222222, 1.333],
            >>>     'bar': 1,
            >>>     'baz': [],
            >>> }
            >>> items2 = {
            >>>     'foo': [1.22222, 1.333],
            >>>     'bar': 1,
            >>>     'baz': [],
            >>> }
            >>> flag, return_info =  ub.indexable_allclose(items1, items2, return_info=True)
            >>> print('return_info = {}'.format(ub.repr2(return_info, nl=1)))
            >>> print('flag = {!r}'.format(flag))
        """
        from ubelt.util_deprecate import schedule_deprecation
        schedule_deprecation(
            'ubelt', 'indexable_allclose', 'function',
            migration=(
                'Use `ub.IndexableWalker(items1).allclose(items2)` instead'
            ))
        walker1 = IndexableWalker(items1)
        walker2 = IndexableWalker(items2)
        return walker1.allclose(walker2, rel_tol=rel_tol, abs_tol=abs_tol,
                                return_info=return_info)

    # Nested = IndexableWalker
    # class Indexable(IndexableWalker):
    #     """
    #     In the future IndexableWalker may simply change to Indexable or maybe Nested
    #     """
    #     ...