Skip to content

Commit

Permalink
Add support for nested dicts (see #10)
Browse files Browse the repository at this point in the history
  • Loading branch information
msiemens committed Jul 24, 2014
1 parent 2b6087e commit 5e606d8
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 0 deletions.
45 changes: 45 additions & 0 deletions tests/test_queries.py
Expand Up @@ -103,3 +103,48 @@ def test(value):
assert not query({'val': 40})
assert not query({'val': '44'})
assert not query({'': None})


def test_has():
query = where('key1').has('key2')

assert query({'key1': {'key2': 1}})
assert not query({'key1': 3})
assert not query({'key1': {'key1': 1}})
assert not query({'key2': {'key1': 1}})

query = where('key1').has('key2') == 1

assert query({'key1': {'key2': 1}})
assert not query({'key1': {'key2': 2}})

# Nested has: key exists
query = where('key1').has('key2').has('key3')
assert query({'key1': {'key2': {'key3': 1}}})
# Not a dict
assert not query({'key1': 1})
assert not query({'key1': {'key2': 1}})
# Wrong key
assert not query({'key1': {'key2': {'key0': 1}}})
assert not query({'key1': {'key0': {'key3': 1}}})
assert not query({'key0': {'key2': {'key3': 1}}})

# Nested has: check for value
query = where('key1').has('key2').has('key3') == 1
assert query({'key1': {'key2': {'key3': 1}}})
assert not query({'key1': {'key2': {'key3': 0}}})

# Test special methods: regex
query = where('key1').has('value').matches(r'\d+')
assert query({'key1': {'value': '123'}})
assert not query({'key2': {'value': '123'}})
assert not query({'key2': {'value': 'abc'}})

# Test special methods: nested has and regex
query = where('key1').has('x').has('y').matches(r'\d+')
assert query({'key1': {'x': {'y': '123'}}})
assert not query({'key1': {'x': {'y': 'abc'}}})

# Test special methods: custom test
query = where('key1').has('int').test(lambda x: x == 3)
assert query({'key1': {'int': 3}})
97 changes: 97 additions & 0 deletions tinydb/queries.py
Expand Up @@ -98,6 +98,18 @@ def test(self, func):
"""
return QueryCustom(self._key, func)

def has(self, key):
"""
Run test on a nested dict.
>>> where('x').has('y') == 2
has 'x' => ('y' == 2)
:param key: the key to search for in the nested dict
:rtype: QueryHas
"""
return QueryHas(self._key, key)

def __eq__(self, other):
"""
Test a dict value for equality.
Expand Down Expand Up @@ -336,3 +348,88 @@ def __call__(self, element):

def __repr__(self):
return '\'{0}\'.test({1})'.format(self._key, self.test)


class QueryHas(Query):
"""
Run a query on a nested dict.
See :meth:`Query.has`
"""

def __init__(self, root, key):
super(QueryHas, self).__init__(key)
self._special = None
self._path = [root] # Store the path to the element to check

def matches(self, regex):
"""
See :meth:`Query.matches`.
"""
self._special = QueryRegex(self._key, regex)
return self

def test(self, func):
"""
See :meth:`Query.test`.
"""
self._special = QueryCustom(self._key, func)
return self

def has(self, key):
"""
See :meth:`Query.has`.
"""
# Nested has: Append old key to path and use given key from now on
self._path.append(self._key)
self._key = key
return self

def __call__(self, element):
"""
See :meth:`Query.__call__`.
"""
# Retrieve value from given path
for key in self._path:
try:
# Check, if requested key exists
if not key in element:
return False

except (KeyError, TypeError):
# We can't continue searching because either ...
# - the element contains a value instead of a dict (TypeError)
# - or doesn't contain the key (KeyError)
return False

# Follow the path and continue searching
element = element[key]

# Verify the element is a dict where we can run the test
# Fixes searching for 'x' => 'y' in {'x': {'y': 2}}
if not isinstance(element, dict):
return False

if self._special:
# Process special test
return self._special(element)
else:
# Process like a normal query
return super(QueryHas, self).__call__(element)

def __repr__(self):
path = self._path

if not self._special and not self._cmp:
path += [self._key]

repr_str = 'has '
# 'key1' => 'key2' => ...
repr_str += '\'' + '\' => \''.join(path) + '\''

if self._special:
repr_str += ' => ({})'.format(self._special)
elif self._cmp:
repr_str += ' => ({})'.format(super(QueryHas, self).__repr__())

return repr_str

0 comments on commit 5e606d8

Please sign in to comment.