Learning TinyDB on Ubuntu - 05 read database

79 阅读1分钟

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.