Introduction
To read the database, TinyDB offers several methods. This article will analyze the code to figure out the relationship between the classes.
table.py
In table.py, it defines class Document and class Table. The class Document is a subclass of dict.
class Document(dict):
def __init__(self, value: Mapping, doc_id: int):
super().__init__(value)
self.doc_id = doc_id
In class Table, it defines search which requires a QueryLike type parameter.
from .queries import QueryLike
def search(self, cond: QueryLike) -> List[Document]:
cached_results = self._query_cache.get(cond)
if cached_results is not None:
return cached_results[:]
docs = [
self.document_class(doc, self.document_id_class(doc_id))
for doc_id, doc in self._read_table().items()
if cond(doc)
]
is_cacheable: Callable[[], bool] = getattr(cond, 'is_cacheable', lambda: True)
if is_cacheable():
self._query_cache[cond] = docs[:]
return docs
In the search method, it uses get which is defined as
from typing import Optional
def get(
self,
cond: Optional[QueryLike] = None,
doc_id: Optional[int] = None,
doc_ids: Optional[List] = None
) -> Optional[Union[Document, List[Document]]]:
...
typing.Optional is a shorthand way to define a type that can be either a specific type or None.
queries.py
In queries.py, it uses Protocol to define abstract class QueryLike.
class QueryLike(Protocol):
def __call__(self, value: Mapping) -> bool: ...
def __hash__(self) -> int: ...
Different from ABC, Protocol is used to restrict classes by checking the type when using it, while ABC restrict classes with a specific class hierarchy. However, the class QueryInstance is an essential class, which is usually read by search. Therefore, this class defined many methods, which are used to claculate the filter rules.
class QueryInstance:
...
def __call__(self, value: Mapping) -> bool: ...
def __hash__(self) -> int: ...
# the required methods of class QueryLike
...
def __eq__(self, other: object): ...
def __and__(self, other: 'QueryInstance') -> 'QueryInstance': ...
def __or__(self, other: 'QueryInstance') -> 'QueryInstance': ...
def __invert__(self) -> 'QueryInstance': ...
# these motheds are used to complete the logic operations
...
Then, the Query is defined as a subclass of QueryInstance. It is initialized by
from typing import Callable, Tuple, Union, Optional
class QueryInstance:
def __init__(self, test: Callable[[Mapping], bool], hashval: Optional[Tuple]):
self._test = test
self._hash = hashval
...
class Query(QueryInstance):
def __init__(self) -> None:
self._path: Tuple[Union[str, Callable], ...] = ()
def notest(_):
raise RuntimeError('Empty query was evaluated')
super().__init__(test=notest, hashval=(None,))
...
Optional is introduced before. Callable, Tuple and Union are respectively used to specify a function or method, including its argument types and return type, specify a fixed-length tuple of elements with specific types and specify a type that can be one of several types. After this, QueryInstance overloads ==, <, <=, >, >= and defines several methods like matches, test, any, all.