Skip to content

Commit

Permalink
fix: make query callables work again
Browse files Browse the repository at this point in the history
Passing a callable as a query used to work but got broken in v4.6.0
due to the necessity of having to distinguish between cacheable
and non-cacheable queries. This was implemented using a new
method named `is_cacheable` that was expected to be present on every
query object -- which of course is not the case for custom queries
that are implemented by passing a callable as a query.

We now only try to call `is_cacheable` if this method is present
on a query object. If it is NOT present, we treat it as cacheable.
This is to keep consistency with TinyDB's earlier behavior which
assumed that all queries are cacheable.

Fixes #454
  • Loading branch information
msiemens committed Jan 18, 2022
1 parent 7e5dc03 commit 1fa99fb
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 5 deletions.
10 changes: 10 additions & 0 deletions tests/test_tinydb.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,3 +681,13 @@ def test_storage_access():
db = TinyDB(storage=MemoryStorage)

assert isinstance(db.storage, MemoryStorage)


def test_lambda_query():
db = TinyDB(storage=MemoryStorage)
db.insert({'foo': 'bar'})

query = lambda doc: doc.get('foo') == 'bar'
query.is_cacheable = lambda: False
assert db.search(query) == [{'foo': 'bar'}]
assert not db._query_cache
9 changes: 5 additions & 4 deletions tinydb/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ class QueryLike(Protocol):
"""
A typing protocol that acts like a query.
Something that we use as a query must have three properties:
Something that we use as a query must have two properties:
1. It must be callable, accepting a `Mapping` object and returning a
boolean that indicates whether the value matches the query, and
2. it must have a stable hash that will be used for query caching.
3. it must declare whether it is cacheable (that is, whether it is immutable).
In addition, to mark a query as non-cacheable (e.g. if it involves
some remote lookup) it needs to have a method called ``is_cacheable``
that returns ``False``.
This query protocol is used to make MyPy correctly support the query
pattern that TinyDB uses.
Expand All @@ -54,8 +57,6 @@ def __call__(self, value: Mapping) -> bool: ...

def __hash__(self): ...

def is_cacheable(self) -> bool: ...


class QueryInstance:
"""
Expand Down
17 changes: 16 additions & 1 deletion tinydb/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,22 @@ def search(self, cond: QueryLike) -> List[Document]:
# Perform the search by applying the query to all documents
docs = [doc for doc in self if cond(doc)]

if cond.is_cacheable():
# Only cache cacheable queries.
#
# This weird `getattr` dance is needed to make MyPy happy as
# it doesn't know that a query might have a `is_cacheable` method
# that is not declared in the `QueryLike` protocol due to it being
# optional.
# See: https://github.com/python/mypy/issues/1424
#
# Note also that by default we expect custom query objects to be
# cacheable (which means they need to have a stable hash value).
# This is to keep consistency with TinyDB's behavior before
# `is_cacheable` was introduced which assumed that all queries
# are cacheable.
is_cacheable: Callable[[], bool] = getattr(cond, 'is_cacheable',
lambda: True)
if is_cacheable():
# Update the query cache
self._query_cache[cond] = docs[:]

Expand Down

0 comments on commit 1fa99fb

Please sign in to comment.