iterutils: add research function

This commit is contained in:
Mahmoud Hashemi 2017-03-11 19:34:58 -08:00
parent 28935c5475
commit a106359085
2 changed files with 85 additions and 2 deletions

View File

@ -979,12 +979,73 @@ def get_path(root, path, default=_UNSET):
return default
return cur
# TODO: get_path/set_path
def research(root, query=lambda p, k, v: True, reraise=False):
"""The :func:`research` function uses :func:`remap` to recurse over
any data nested in *root*, and find values which match a given
criterion, specified by the *query* callable.
Results are returned as a list of ``(path, value)`` pairs. The
paths are tuples in the same format accepted by
:func:`get_path`. This can be useful for comparing values nested
in two or more different structures.
Here's a simple example that finds all integers:
>>> root = {'a': {'b': 1, 'c': (2, 'd', 3)}, 'e': None}
>>> res = research(root, query=lambda p, k, v: isinstance(v, int))
>>> print(sorted(res))
[(('a', 'b'), 1), (('a', 'c', 0), 2), (('a', 'c', 2), 3)]
Note how *query* follows the same, familiar ``path, key, value``
signature as the ``visit`` and ``enter`` functions on
:func:`remap`, and returns a :class:`bool`.
Args:
root: The target object to search. Supports the same types of
objects as :func:`remap`, including :class:`list`,
:class:`tuple`, :class:`dict`, and :class:`set`.
query (callable): The function called on every object to
determine whether to include it in the search results. The
callable must accept three arguments, *path*, *key*, and
*value*, commonly abbreviated *p*, *k*, and *v*, same as
*enter* and *visit* from :func:`remap`.
reraise (bool): Whether to reraise exceptions raised by *query*
or to simply drop the result that caused the error.
With :func:`research` it's easy to inspect the details of a data
structure, like finding values that are at a certain depth (using
``len(p)``) and much more. If more advanced functionality is
needed, check out the code and make your own :func:`remap`
wrapper, and consider `submitting a patch`_!
.. _submitting a patch: https://github.com/mahmoud/boltons/pulls
"""
ret = []
if not callable(query):
raise TypeError('query expected callable, not: %r' % query)
def enter(path, key, value):
try:
if query(path, key, value):
ret.append((path + (key,), value))
except Exception:
if reraise:
raise
return default_enter(path, key, value)
remap(root, enter=enter)
return ret
# TODO: recollect()
# TODO: refilter()
# TODO: reiter()
# GUID iterators: 10x faster and somewhat more compact than uuid.
# GUID iterators: 10x faster and somewhat more compact than uuid.
class GUIDerator(object):
"""The GUIDerator is an iterator that yields a globally-unique

View File

@ -4,6 +4,7 @@ import pytest
from boltons.dictutils import OMD
from boltons.iterutils import (first,
remap,
research,
default_enter,
default_exit,
get_path)
@ -356,6 +357,27 @@ class TestGetPath(object):
assert get_path(root, 'key.0') == 'test'
def test_research():
root = {}
with pytest.raises(TypeError):
research(root, query=None)
root = {'a': 'a'}
res = research(root, query=lambda p, k, v: v == 'a')
assert len(res) == 1
assert res[0] == (('a',), 'a')
def broken_query(p, k, v):
raise RuntimeError()
with pytest.raises(RuntimeError):
research(root, broken_query, reraise=True)
# empty results with default, reraise=False
assert research(root, broken_query) == []
def test_backoff_basic():
from boltons.iterutils import backoff