• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

msiemens / tinydb / 11220453171

07 Oct 2024 05:06PM UTC coverage: 98.817% (+0.009%) from 98.808%
11220453171

push

github

msiemens
chore: add param to docstring

229 of 235 branches covered (97.45%)

Branch coverage included in aggregate %.

606 of 610 relevant lines covered (99.34%)

19.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.38
/tinydb/table.py
1
"""
2
This module implements tables, the central place for accessing and manipulating
3
data in TinyDB.
4
"""
5

6
from typing import (
20✔
7
    Callable,
8
    Dict,
9
    Iterable,
10
    Iterator,
11
    List,
12
    Mapping,
13
    Optional,
14
    Union,
15
    cast,
16
    Tuple
17
)
18

19
from .queries import QueryLike
20✔
20
from .storages import Storage
20✔
21
from .utils import LRUCache
20✔
22

23
__all__ = ('Document', 'Table')
20✔
24

25

26
class Document(dict):
20✔
27
    """
28
    A document stored in the database.
29

30
    This class provides a way to access both a document's content and
31
    its ID using ``doc.doc_id``.
32
    """
33

34
    def __init__(self, value: Mapping, doc_id: int):
20✔
35
        super().__init__(value)
20✔
36
        self.doc_id = doc_id
20✔
37

38

39
class Table:
20✔
40
    """
41
    Represents a single TinyDB table.
42

43
    It provides methods for accessing and manipulating documents.
44

45
    .. admonition:: Query Cache
46

47
        As an optimization, a query cache is implemented using a
48
        :class:`~tinydb.utils.LRUCache`. This class mimics the interface of
49
        a normal ``dict``, but starts to remove the least-recently used entries
50
        once a threshold is reached.
51

52
        The query cache is updated on every search operation. When writing
53
        data, the whole cache is discarded as the query results may have
54
        changed.
55

56
    .. admonition:: Customization
57

58
        For customization, the following class variables can be set:
59

60
        - ``document_class`` defines the class that is used to represent
61
          documents,
62
        - ``document_id_class`` defines the class that is used to represent
63
          document IDs,
64
        - ``query_cache_class`` defines the class that is used for the query
65
          cache
66
        - ``default_query_cache_capacity`` defines the default capacity of
67
          the query cache
68

69
        .. versionadded:: 4.0
70

71

72
    :param storage: The storage instance to use for this table
73
    :param name: The table name
74
    :param cache_size: Maximum capacity of query cache
75
    :param persist_empty: Store new table even with no operations on it
76
    """
77

78
    #: The class used to represent documents
79
    #:
80
    #: .. versionadded:: 4.0
81
    document_class = Document
20✔
82

83
    #: The class used to represent a document ID
84
    #:
85
    #: .. versionadded:: 4.0
86
    document_id_class = int
20✔
87

88
    #: The class used for caching query results
89
    #:
90
    #: .. versionadded:: 4.0
91
    query_cache_class = LRUCache
20✔
92

93
    #: The default capacity of the query cache
94
    #:
95
    #: .. versionadded:: 4.0
96
    default_query_cache_capacity = 10
20✔
97

98
    def __init__(
20✔
99
        self,
100
        storage: Storage,
101
        name: str,
102
        cache_size: int = default_query_cache_capacity,
103
        persist_empty: bool = False
104
    ):
105
        """
106
        Create a table instance.
107
        """
108

109
        self._storage = storage
20✔
110
        self._name = name
20✔
111
        self._query_cache: LRUCache[QueryLike, List[Document]] \
20✔
112
            = self.query_cache_class(capacity=cache_size)
113

114
        self._next_id = None
20✔
115
        if persist_empty:
20✔
116
            self._update_table(lambda table: table.clear())
20✔
117

118
    def __repr__(self):
119
        args = [
120
            'name={!r}'.format(self.name),
121
            'total={}'.format(len(self)),
122
            'storage={}'.format(self._storage),
123
        ]
124

125
        return '<{} {}>'.format(type(self).__name__, ', '.join(args))
126

127
    @property
20✔
128
    def name(self) -> str:
20✔
129
        """
130
        Get the table name.
131
        """
132
        return self._name
20✔
133

134
    @property
20✔
135
    def storage(self) -> Storage:
20✔
136
        """
137
        Get the table storage instance.
138
        """
139
        return self._storage
×
140

141
    def insert(self, document: Mapping) -> int:
20✔
142
        """
143
        Insert a new document into the table.
144

145
        :param document: the document to insert
146
        :returns: the inserted document's ID
147
        """
148

149
        # Make sure the document implements the ``Mapping`` interface
150
        if not isinstance(document, Mapping):
20✔
151
            raise ValueError('Document is not a Mapping')
20✔
152

153
        # First, we get the document ID for the new document
154
        if isinstance(document, self.document_class):
20✔
155
            # For a `Document` object we use the specified ID
156
            doc_id = document.doc_id
20✔
157

158
            # We also reset the stored next ID so the next insert won't
159
            # re-use document IDs by accident when storing an old value
160
            self._next_id = None
20✔
161
        else:
162
            # In all other cases we use the next free ID
163
            doc_id = self._get_next_id()
20✔
164

165
        # Now, we update the table and add the document
166
        def updater(table: dict):
20✔
167
            if doc_id in table:
20✔
168
                raise ValueError(f'Document with ID {str(doc_id)} '
20✔
169
                                 f'already exists')
170

171
            # By calling ``dict(document)`` we convert the data we got to a
172
            # ``dict`` instance even if it was a different class that
173
            # implemented the ``Mapping`` interface
174
            table[doc_id] = dict(document)
20✔
175

176
        # See below for details on ``Table._update``
177
        self._update_table(updater)
20✔
178

179
        return doc_id
20✔
180

181
    def insert_multiple(self, documents: Iterable[Mapping]) -> List[int]:
20✔
182
        """
183
        Insert multiple documents into the table.
184

185
        :param documents: an Iterable of documents to insert
186
        :returns: a list containing the inserted documents' IDs
187
        """
188
        doc_ids = []
20✔
189

190
        def updater(table: dict):
20✔
191
            for document in documents:
20✔
192

193
                # Make sure the document implements the ``Mapping`` interface
194
                if not isinstance(document, Mapping):
20✔
195
                    raise ValueError('Document is not a Mapping')
20✔
196

197
                if isinstance(document, self.document_class):
20✔
198
                    # Check if document does not override an existing document
199
                    if document.doc_id in table:
20✔
200
                        raise ValueError(
20✔
201
                            f'Document with ID {str(document.doc_id)} '
202
                            f'already exists'
203
                        )
204

205
                    # Store the doc_id, so we can return all document IDs
206
                    # later. Then save the document with its doc_id and
207
                    # skip the rest of the current loop
208
                    doc_id = document.doc_id
20✔
209
                    doc_ids.append(doc_id)
20✔
210
                    table[doc_id] = dict(document)
20✔
211
                    continue
20✔
212

213
                # Generate new document ID for this document
214
                # Store the doc_id, so we can return all document IDs
215
                # later, then save the document with the new doc_id
216
                doc_id = self._get_next_id()
20✔
217
                doc_ids.append(doc_id)
20✔
218
                table[doc_id] = dict(document)
20✔
219

220
        # See below for details on ``Table._update``
221
        self._update_table(updater)
20✔
222

223
        return doc_ids
20✔
224

225
    def all(self) -> List[Document]:
20✔
226
        """
227
        Get all documents stored in the table.
228

229
        :returns: a list with all documents.
230
        """
231

232
        # iter(self) (implemented in Table.__iter__ provides an iterator
233
        # that returns all documents in this table. We use it to get a list
234
        # of all documents by using the ``list`` constructor to perform the
235
        # conversion.
236

237
        return list(iter(self))
20✔
238

239
    def search(self, cond: QueryLike) -> List[Document]:
20✔
240
        """
241
        Search for all documents matching a 'where' cond.
242

243
        :param cond: the condition to check against
244
        :returns: list of matching documents
245
        """
246

247
        # First, we check the query cache to see if it has results for this
248
        # query
249
        cached_results = self._query_cache.get(cond)
20✔
250
        if cached_results is not None:
20✔
251
            return cached_results[:]
20✔
252

253
        # Perform the search by applying the query to all documents.
254
        # Then, only if the document matches the query, convert it
255
        # to the document class and document ID class.
256
        docs = [
20✔
257
            self.document_class(doc, self.document_id_class(doc_id))
258
            for doc_id, doc in self._read_table().items()
259
            if cond(doc)
260
        ]
261

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

281
        return docs
20✔
282

283
    def get(
20✔
284
        self,
285
        cond: Optional[QueryLike] = None,
286
        doc_id: Optional[int] = None,
287
        doc_ids: Optional[List] = None
288
    ) -> Optional[Union[Document, List[Document]]]:
289
        """
290
        Get exactly one document specified by a query or a document ID.
291
        However, if multiple document IDs are given then returns all
292
        documents in a list.
293
        
294
        Returns ``None`` if the document doesn't exist.
295

296
        :param cond: the condition to check against
297
        :param doc_id: the document's ID
298
        :param doc_ids: the document's IDs(multiple)
299

300
        :returns: the document(s) or ``None``
301
        """
302
        table = self._read_table()
20✔
303

304
        if doc_id is not None:
20✔
305
            # Retrieve a document specified by its ID
306
            raw_doc = table.get(str(doc_id), None)
20✔
307

308
            if raw_doc is None:
20✔
309
                return None
20✔
310

311
            # Convert the raw data to the document class
312
            return self.document_class(raw_doc, doc_id)
20✔
313

314
        elif doc_ids is not None:
20✔
315
            # Filter the table by extracting out all those documents which
316
            # have doc id specified in the doc_id list.
317

318
            # Since document IDs will be unique, we make it a set to ensure
319
            # constant time lookup
320
            doc_ids_set = set(str(doc_id) for doc_id in doc_ids)
20✔
321

322
            # Now return the filtered documents in form of list
323
            return [
20✔
324
                self.document_class(doc, self.document_id_class(doc_id))
325
                for doc_id, doc in table.items()
326
                if doc_id in doc_ids_set
327
            ]
328

329
        elif cond is not None:
20✔
330
            # Find a document specified by a query
331
            # The trailing underscore in doc_id_ is needed so MyPy
332
            # doesn't think that `doc_id_` (which is a string) needs
333
            # to have the same type as `doc_id` which is this function's
334
            # parameter and is an optional `int`.
335
            for doc_id_, doc in self._read_table().items():
20✔
336
                if cond(doc):
20✔
337
                    return self.document_class(
20✔
338
                        doc,
339
                        self.document_id_class(doc_id_)
340
                    )
341

342
            return None
20✔
343

344
        raise RuntimeError('You have to pass either cond or doc_id or doc_ids')
20✔
345

346
    def contains(
20✔
347
        self,
348
        cond: Optional[QueryLike] = None,
349
        doc_id: Optional[int] = None
350
    ) -> bool:
351
        """
352
        Check whether the database contains a document matching a query or
353
        an ID.
354

355
        If ``doc_id`` is set, it checks if the db contains the specified ID.
356

357
        :param cond: the condition use
358
        :param doc_id: the document ID to look for
359
        """
360
        if doc_id is not None:
20✔
361
            # Documents specified by ID
362
            return self.get(doc_id=doc_id) is not None
20✔
363

364
        elif cond is not None:
20✔
365
            # Document specified by condition
366
            return self.get(cond) is not None
20✔
367

368
        raise RuntimeError('You have to pass either cond or doc_id')
20✔
369

370
    def update(
20✔
371
        self,
372
        fields: Union[Mapping, Callable[[Mapping], None]],
373
        cond: Optional[QueryLike] = None,
374
        doc_ids: Optional[Iterable[int]] = None,
375
    ) -> List[int]:
376
        """
377
        Update all matching documents to have a given set of fields.
378

379
        :param fields: the fields that the matching documents will have
380
                       or a method that will update the documents
381
        :param cond: which documents to update
382
        :param doc_ids: a list of document IDs
383
        :returns: a list containing the updated document's ID
384
        """
385

386
        # Define the function that will perform the update
387
        if callable(fields):
20✔
388
            def perform_update(table, doc_id):
20✔
389
                # Update documents by calling the update function provided by
390
                # the user
391
                fields(table[doc_id])
20✔
392
        else:
393
            def perform_update(table, doc_id):
20✔
394
                # Update documents by setting all fields from the provided data
395
                table[doc_id].update(fields)
20✔
396

397
        if doc_ids is not None:
20✔
398
            # Perform the update operation for documents specified by a list
399
            # of document IDs
400

401
            updated_ids = list(doc_ids)
20✔
402

403
            def updater(table: dict):
20✔
404
                # Call the processing callback with all document IDs
405
                for doc_id in updated_ids:
20✔
406
                    perform_update(table, doc_id)
20✔
407

408
            # Perform the update operation (see _update_table for details)
409
            self._update_table(updater)
20✔
410

411
            return updated_ids
20✔
412

413
        elif cond is not None:
20✔
414
            # Perform the update operation for documents specified by a query
415

416
            # Collect affected doc_ids
417
            updated_ids = []
20✔
418

419
            def updater(table: dict):
20✔
420
                _cond = cast(QueryLike, cond)
20✔
421

422
                # We need to convert the keys iterator to a list because
423
                # we may remove entries from the ``table`` dict during
424
                # iteration and doing this without the list conversion would
425
                # result in an exception (RuntimeError: dictionary changed size
426
                # during iteration)
427
                for doc_id in list(table.keys()):
20✔
428
                    # Pass through all documents to find documents matching the
429
                    # query. Call the processing callback with the document ID
430
                    if _cond(table[doc_id]):
20✔
431
                        # Add ID to list of updated documents
432
                        updated_ids.append(doc_id)
20✔
433

434
                        # Perform the update (see above)
435
                        perform_update(table, doc_id)
20✔
436

437
            # Perform the update operation (see _update_table for details)
438
            self._update_table(updater)
20✔
439

440
            return updated_ids
20✔
441

442
        else:
443
            # Update all documents unconditionally
444

445
            updated_ids = []
20✔
446

447
            def updater(table: dict):
20✔
448
                # Process all documents
449
                for doc_id in list(table.keys()):
20✔
450
                    # Add ID to list of updated documents
451
                    updated_ids.append(doc_id)
20✔
452

453
                    # Perform the update (see above)
454
                    perform_update(table, doc_id)
20✔
455

456
            # Perform the update operation (see _update_table for details)
457
            self._update_table(updater)
20✔
458

459
            return updated_ids
20✔
460

461
    def update_multiple(
20✔
462
        self,
463
        updates: Iterable[
464
            Tuple[Union[Mapping, Callable[[Mapping], None]], QueryLike]
465
        ],
466
    ) -> List[int]:
467
        """
468
        Update all matching documents to have a given set of fields.
469

470
        :returns: a list containing the updated document's ID
471
        """
472

473
        # Define the function that will perform the update
474
        def perform_update(fields, table, doc_id):
20✔
475
            if callable(fields):
20✔
476
                # Update documents by calling the update function provided
477
                # by the user
478
                fields(table[doc_id])
20✔
479
            else:
480
                # Update documents by setting all fields from the provided
481
                # data
482
                table[doc_id].update(fields)
20✔
483

484
        # Perform the update operation for documents specified by a query
485

486
        # Collect affected doc_ids
487
        updated_ids = []
20✔
488

489
        def updater(table: dict):
20✔
490
            # We need to convert the keys iterator to a list because
491
            # we may remove entries from the ``table`` dict during
492
            # iteration and doing this without the list conversion would
493
            # result in an exception (RuntimeError: dictionary changed size
494
            # during iteration)
495
            for doc_id in list(table.keys()):
20✔
496
                for fields, cond in updates:
20✔
497
                    _cond = cast(QueryLike, cond)
20✔
498

499
                    # Pass through all documents to find documents matching the
500
                    # query. Call the processing callback with the document ID
501
                    if _cond(table[doc_id]):
20✔
502
                        # Add ID to list of updated documents
503
                        updated_ids.append(doc_id)
20✔
504

505
                        # Perform the update (see above)
506
                        perform_update(fields, table, doc_id)
20✔
507

508
        # Perform the update operation (see _update_table for details)
509
        self._update_table(updater)
20✔
510

511
        return updated_ids
20✔
512

513
    def upsert(self, document: Mapping, cond: Optional[QueryLike] = None) -> List[int]:
20✔
514
        """
515
        Update documents, if they exist, insert them otherwise.
516

517
        Note: This will update *all* documents matching the query. Document
518
        argument can be a tinydb.table.Document object if you want to specify a
519
        doc_id.
520

521
        :param document: the document to insert or the fields to update
522
        :param cond: which document to look for, optional if you've passed a
523
        Document with a doc_id
524
        :returns: a list containing the updated documents' IDs
525
        """
526

527
        # Extract doc_id
528
        if isinstance(document, self.document_class) and hasattr(document, 'doc_id'):
20✔
529
            doc_ids: Optional[List[int]] = [document.doc_id]
20✔
530
        else:
531
            doc_ids = None
20✔
532

533
        # Make sure we can actually find a matching document
534
        if doc_ids is None and cond is None:
20✔
535
            raise ValueError("If you don't specify a search query, you must "
20✔
536
                             "specify a doc_id. Hint: use a table.Document "
537
                             "object.")
538

539
        # Perform the update operation
540
        try:
20✔
541
            updated_docs: Optional[List[int]] = self.update(document, cond, doc_ids)
20✔
542
        except KeyError:
20✔
543
            # This happens when a doc_id is specified, but it's missing
544
            updated_docs = None
20✔
545

546
        # If documents have been updated: return their IDs
547
        if updated_docs:
20✔
548
            return updated_docs
20✔
549

550
        # There are no documents that match the specified query -> insert the
551
        # data as a new document
552
        return [self.insert(document)]
20✔
553

554
    def remove(
20✔
555
        self,
556
        cond: Optional[QueryLike] = None,
557
        doc_ids: Optional[Iterable[int]] = None,
558
    ) -> List[int]:
559
        """
560
        Remove all matching documents.
561

562
        :param cond: the condition to check against
563
        :param doc_ids: a list of document IDs
564
        :returns: a list containing the removed documents' ID
565
        """
566
        if doc_ids is not None:
20✔
567
            # This function returns the list of IDs for the documents that have
568
            # been removed. When removing documents identified by a set of
569
            # document IDs, it's this list of document IDs we need to return
570
            # later.
571
            # We convert the document ID iterator into a list, so we can both
572
            # use the document IDs to remove the specified documents and
573
            # to return the list of affected document IDs
574
            removed_ids = list(doc_ids)
20✔
575

576
            def updater(table: dict):
20✔
577
                for doc_id in removed_ids:
20✔
578
                    table.pop(doc_id)
20✔
579

580
            # Perform the remove operation
581
            self._update_table(updater)
20✔
582

583
            return removed_ids
20✔
584

585
        if cond is not None:
20✔
586
            removed_ids = []
20✔
587

588
            # This updater function will be called with the table data
589
            # as its first argument. See ``Table._update`` for details on this
590
            # operation
591
            def updater(table: dict):
20✔
592
                # We need to convince MyPy (the static type checker) that
593
                # the ``cond is not None`` invariant still holds true when
594
                # the updater function is called
595
                _cond = cast(QueryLike, cond)
20✔
596

597
                # We need to convert the keys iterator to a list because
598
                # we may remove entries from the ``table`` dict during
599
                # iteration and doing this without the list conversion would
600
                # result in an exception (RuntimeError: dictionary changed size
601
                # during iteration)
602
                for doc_id in list(table.keys()):
20✔
603
                    if _cond(table[doc_id]):
20✔
604
                        # Add document ID to list of removed document IDs
605
                        removed_ids.append(doc_id)
20✔
606

607
                        # Remove document from the table
608
                        table.pop(doc_id)
20✔
609

610
            # Perform the remove operation
611
            self._update_table(updater)
20✔
612

613
            return removed_ids
20✔
614

615
        raise RuntimeError('Use truncate() to remove all documents')
20✔
616

617
    def truncate(self) -> None:
20✔
618
        """
619
        Truncate the table by removing all documents.
620
        """
621

622
        # Update the table by resetting all data
623
        self._update_table(lambda table: table.clear())
20✔
624

625
        # Reset document ID counter
626
        self._next_id = None
20✔
627

628
    def count(self, cond: QueryLike) -> int:
20✔
629
        """
630
        Count the documents matching a query.
631

632
        :param cond: the condition use
633
        """
634

635
        return len(self.search(cond))
20✔
636

637
    def clear_cache(self) -> None:
20✔
638
        """
639
        Clear the query cache.
640
        """
641

642
        self._query_cache.clear()
20✔
643

644
    def __len__(self):
20✔
645
        """
646
        Count the total number of documents in this table.
647
        """
648

649
        return len(self._read_table())
20✔
650

651
    def __iter__(self) -> Iterator[Document]:
20✔
652
        """
653
        Iterate over all documents stored in the table.
654

655
        :returns: an iterator over all documents.
656
        """
657

658
        # Iterate all documents and their IDs
659
        for doc_id, doc in self._read_table().items():
20✔
660
            # Convert documents to the document class
661
            yield self.document_class(doc, self.document_id_class(doc_id))
20✔
662

663
    def _get_next_id(self):
20✔
664
        """
665
        Return the ID for a newly inserted document.
666
        """
667

668
        # If we already know the next ID
669
        if self._next_id is not None:
20✔
670
            next_id = self._next_id
20✔
671
            self._next_id = next_id + 1
20✔
672

673
            return next_id
20✔
674

675
        # Determine the next document ID by finding out the max ID value
676
        # of the current table documents
677

678
        # Read the table documents
679
        table = self._read_table()
20✔
680

681
        # If the table is empty, set the initial ID
682
        if not table:
20✔
683
            next_id = 1
20✔
684
            self._next_id = next_id + 1
20✔
685

686
            return next_id
20✔
687

688
        # Determine the next ID based on the maximum ID that's currently in use
689
        max_id = max(self.document_id_class(i) for i in table.keys())
20✔
690
        next_id = max_id + 1
20✔
691

692
        # The next ID we will return AFTER this call needs to be larger than
693
        # the current next ID we calculated
694
        self._next_id = next_id + 1
20✔
695

696
        return next_id
20✔
697

698
    def _read_table(self) -> Dict[str, Mapping]:
20✔
699
        """
700
        Read the table data from the underlying storage.
701

702
        Documents and doc_ids are NOT yet transformed, as
703
        we may not want to convert *all* documents when returning
704
        only one document for example.
705
        """
706

707
        # Retrieve the tables from the storage
708
        tables = self._storage.read()
20✔
709

710
        if tables is None:
20✔
711
            # The database is empty
712
            return {}
20✔
713

714
        # Retrieve the current table's data
715
        try:
20✔
716
            table = tables[self.name]
20✔
717
        except KeyError:
20✔
718
            # The table does not exist yet, so it is empty
719
            return {}
20✔
720

721
        return table
20✔
722

723
    def _update_table(self, updater: Callable[[Dict[int, Mapping]], None]):
20✔
724
        """
725
        Perform a table update operation.
726

727
        The storage interface used by TinyDB only allows to read/write the
728
        complete database data, but not modifying only portions of it. Thus,
729
        to only update portions of the table data, we first perform a read
730
        operation, perform the update on the table data and then write
731
        the updated data back to the storage.
732

733
        As a further optimization, we don't convert the documents into the
734
        document class, as the table data will *not* be returned to the user.
735
        """
736

737
        tables = self._storage.read()
20✔
738

739
        if tables is None:
20✔
740
            # The database is empty
741
            tables = {}
20✔
742

743
        try:
20✔
744
            raw_table = tables[self.name]
20✔
745
        except KeyError:
20✔
746
            # The table does not exist yet, so it is empty
747
            raw_table = {}
20✔
748

749
        # Convert the document IDs to the document ID class.
750
        # This is required as the rest of TinyDB expects the document IDs
751
        # to be an instance of ``self.document_id_class`` but the storage
752
        # might convert dict keys to strings.
753
        table = {
20✔
754
            self.document_id_class(doc_id): doc
755
            for doc_id, doc in raw_table.items()
756
        }
757

758
        # Perform the table update operation
759
        updater(table)
20✔
760

761
        # Convert the document IDs back to strings.
762
        # This is required as some storages (most notably the JSON file format)
763
        # don't support IDs other than strings.
764
        tables[self.name] = {
20✔
765
            str(doc_id): doc
766
            for doc_id, doc in table.items()
767
        }
768

769
        # Write the newly updated data back to the storage
770
        self._storage.write(tables)
20✔
771

772
        # Clear the query cache, as the table contents have changed
773
        self.clear_cache()
20✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc