Skip to content

Commit 6f03070

Browse files
PersistentDict: better docs, type annotations (#212)
* better docs, type annotations * more annotations * add to README * make it generic * fix doc * complete annotations
1 parent 3e9d9d2 commit 6f03070

5 files changed

Lines changed: 142 additions & 86 deletions

File tree

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ nonetheless, here's what's on offer:
2424
GvR's monkeypatch_xxx() hack, the elusive `flatten`, and much more.
2525
* Batch job submission, `pytools.batchjob`.
2626
* A lexer, `pytools.lex`.
27+
* A persistent key-value store, `pytools.persistent_dict`.
2728

2829
Links:
2930

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"pytest": ("https://docs.pytest.org/en/stable/", None),
3333
"setuptools": ("https://setuptools.pypa.io/en/latest/", None),
3434
"python": ("https://docs.python.org/3", None),
35+
"platformdirs": ("https://platformdirs.readthedocs.io/en/latest/", None),
3536
}
3637

3738
nitpicky = True

pytools/persistent_dict.py

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import sys
3838
from dataclasses import fields as dc_fields, is_dataclass
3939
from enum import Enum
40-
from typing import TYPE_CHECKING, Any, Mapping, Protocol
40+
from typing import TYPE_CHECKING, Any, Generic, Mapping, Optional, Protocol, TypeVar
4141

4242

4343
if TYPE_CHECKING:
@@ -75,6 +75,18 @@
7575
.. autoclass:: KeyBuilder
7676
.. autoclass:: PersistentDict
7777
.. autoclass:: WriteOncePersistentDict
78+
79+
80+
Internal stuff that is only here because the documentation tool wants it
81+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82+
83+
.. class:: K
84+
85+
A type variable for the key type of a :class:`PersistentDict`.
86+
87+
.. class:: V
88+
89+
A type variable for the value type of a :class:`PersistentDict`.
7890
"""
7991

8092

@@ -478,8 +490,14 @@ class CollisionWarning(UserWarning):
478490
pass
479491

480492

481-
class _PersistentDictBase:
482-
def __init__(self, identifier, key_builder=None, container_dir=None):
493+
K = TypeVar("K")
494+
V = TypeVar("V")
495+
496+
497+
class _PersistentDictBase(Generic[K, V]):
498+
def __init__(self, identifier: str,
499+
key_builder: Optional[KeyBuilder] = None,
500+
container_dir: Optional[str] = None) -> None:
483501
self.identifier = identifier
484502

485503
if key_builder is None:
@@ -509,32 +527,37 @@ def __init__(self, identifier, key_builder=None, container_dir=None):
509527
self._make_container_dir()
510528

511529
@staticmethod
512-
def _warn(msg, category=UserWarning, stacklevel=0):
530+
def _warn(msg: str, category: Any = UserWarning, stacklevel: int = 0) -> None:
513531
from warnings import warn
514532
warn(msg, category, stacklevel=1 + stacklevel)
515533

516-
def store_if_not_present(self, key, value, _stacklevel=0):
534+
def store_if_not_present(self, key: K, value: V,
535+
_stacklevel: int = 0) -> None:
536+
"""Store (*key*, *value*) if *key* is not already present."""
517537
self.store(key, value, _skip_if_present=True, _stacklevel=1 + _stacklevel)
518538

519-
def store(self, key, value, _skip_if_present=False, _stacklevel=0):
539+
def store(self, key: K, value: V, _skip_if_present: bool = False,
540+
_stacklevel: int = 0) -> None:
541+
"""Store (*key*, *value*) in the dictionary."""
520542
raise NotImplementedError()
521543

522-
def fetch(self, key, _stacklevel=0):
544+
def fetch(self, key: K, _stacklevel: int = 0) -> V:
545+
"""Return the value associated with *key* in the dictionary."""
523546
raise NotImplementedError()
524547

525548
@staticmethod
526-
def _read(path):
549+
def _read(path: str) -> V:
527550
from pickle import load
528551
with open(path, "rb") as inf:
529552
return load(inf)
530553

531554
@staticmethod
532-
def _write(path, value):
555+
def _write(path: str, value: V) -> None:
533556
from pickle import HIGHEST_PROTOCOL, dump
534557
with open(path, "wb") as outf:
535558
dump(value, outf, protocol=HIGHEST_PROTOCOL)
536559

537-
def _item_dir(self, hexdigest_key):
560+
def _item_dir(self, hexdigest_key: str) -> str:
538561
from os.path import join
539562

540563
# Some file systems limit the number of directories in a directory.
@@ -546,22 +569,23 @@ def _item_dir(self, hexdigest_key):
546569
hexdigest_key[3:6],
547570
hexdigest_key[6:])
548571

549-
def _key_file(self, hexdigest_key):
572+
def _key_file(self, hexdigest_key: str) -> str:
550573
from os.path import join
551574
return join(self._item_dir(hexdigest_key), "key")
552575

553-
def _contents_file(self, hexdigest_key):
576+
def _contents_file(self, hexdigest_key: str) -> str:
554577
from os.path import join
555578
return join(self._item_dir(hexdigest_key), "contents")
556579

557-
def _lock_file(self, hexdigest_key):
580+
def _lock_file(self, hexdigest_key: str) -> str:
558581
from os.path import join
559582
return join(self.container_dir, str(hexdigest_key) + ".lock")
560583

561-
def _make_container_dir(self):
584+
def _make_container_dir(self) -> None:
585+
"""Create the container directory to store the dictionary."""
562586
os.makedirs(self.container_dir, exist_ok=True)
563587

564-
def _collision_check(self, key, stored_key, _stacklevel):
588+
def _collision_check(self, key: K, stored_key: K, _stacklevel: int) -> None:
565589
if stored_key != key:
566590
# Key collision, oh well.
567591
self._warn(f"{self.identifier}: key collision in cache at "
@@ -577,13 +601,16 @@ def _collision_check(self, key, stored_key, _stacklevel):
577601
stored_key == key # pylint:disable=pointless-statement # noqa: B015
578602
raise NoSuchEntryCollisionError(key)
579603

580-
def __getitem__(self, key):
604+
def __getitem__(self, key: K) -> V:
605+
"""Return the value associated with *key* in the dictionary."""
581606
return self.fetch(key, _stacklevel=1)
582607

583-
def __setitem__(self, key, value):
608+
def __setitem__(self, key: K, value: V) -> None:
609+
"""Store (*key*, *value*) in the dictionary."""
584610
self.store(key, value, _stacklevel=1)
585611

586-
def clear(self):
612+
def clear(self) -> None:
613+
"""Remove all entries from the dictionary."""
587614
try:
588615
shutil.rmtree(self.container_dir)
589616
except OSError as e:
@@ -593,8 +620,9 @@ def clear(self):
593620
self._make_container_dir()
594621

595622

596-
class WriteOncePersistentDict(_PersistentDictBase):
597-
"""A concurrent disk-backed dictionary that disallows overwriting/deletion.
623+
class WriteOncePersistentDict(_PersistentDictBase[K, V]):
624+
"""A concurrent disk-backed dictionary that disallows overwriting/
625+
deletion (but allows removing all entries).
598626
599627
Compared with :class:`PersistentDict`, this class has faster
600628
retrieval times because it uses an LRU cache to cache entries in memory.
@@ -608,14 +636,19 @@ class WriteOncePersistentDict(_PersistentDictBase):
608636
.. automethod:: store_if_not_present
609637
.. automethod:: fetch
610638
"""
611-
def __init__(self, identifier, key_builder=None, container_dir=None,
612-
in_mem_cache_size=256):
639+
def __init__(self, identifier: str,
640+
key_builder: Optional[KeyBuilder] = None,
641+
container_dir: Optional[str] = None,
642+
in_mem_cache_size: int = 256) -> None:
613643
"""
614644
:arg identifier: a file-name-compatible string identifying this
615645
dictionary
616646
:arg key_builder: a subclass of :class:`KeyBuilder`
647+
:arg container_dir: the directory in which to store this
648+
dictionary. If ``None``, the default cache directory from
649+
:func:`platformdirs.user_cache_dir` is used
617650
:arg in_mem_cache_size: retain an in-memory cache of up to
618-
*in_mem_cache_size* items
651+
*in_mem_cache_size* items (with an LRU replacement policy)
619652
"""
620653
_PersistentDictBase.__init__(self, identifier, key_builder, container_dir)
621654
self._in_mem_cache_size = in_mem_cache_size
@@ -624,12 +657,14 @@ def __init__(self, identifier, key_builder=None, container_dir=None,
624657

625658
def clear_in_mem_cache(self) -> None:
626659
"""
660+
Clear the in-memory cache of this dictionary.
661+
627662
.. versionadded:: 2023.1.1
628663
"""
629664

630665
self._fetch.cache_clear()
631666

632-
def _spin_until_removed(self, lock_file, stacklevel):
667+
def _spin_until_removed(self, lock_file: str, stacklevel: int) -> None:
633668
from os.path import exists
634669

635670
attempts = 0
@@ -649,7 +684,8 @@ def _spin_until_removed(self, lock_file, stacklevel):
649684
f"on the lock file '{lock_file}'"
650685
"--something is wrong")
651686

652-
def store(self, key, value, _skip_if_present=False, _stacklevel=0):
687+
def store(self, key: K, value: V, _skip_if_present: bool = False,
688+
_stacklevel: int = 0) -> None:
653689
hexdigest_key = self.key_builder(key)
654690

655691
cleanup_m = CleanupManager()
@@ -682,7 +718,7 @@ def store(self, key, value, _skip_if_present=False, _stacklevel=0):
682718
finally:
683719
cleanup_m.clean_up()
684720

685-
def fetch(self, key, _stacklevel=0):
721+
def fetch(self, key: K, _stacklevel: int = 0) -> Any:
686722
hexdigest_key = self.key_builder(key)
687723

688724
(stored_key, stored_value) = self._fetch(hexdigest_key, 1 + _stacklevel)
@@ -691,7 +727,8 @@ def fetch(self, key, _stacklevel=0):
691727

692728
return stored_value
693729

694-
def _fetch(self, hexdigest_key, _stacklevel=0): # pylint:disable=method-hidden
730+
def _fetch(self, hexdigest_key: str, # pylint:disable=method-hidden
731+
_stacklevel: int = 0) -> V:
695732
# This is separate from fetch() to allow for LRU caching
696733

697734
# {{{ check path exists and is unlocked
@@ -748,12 +785,12 @@ def _fetch(self, hexdigest_key, _stacklevel=0): # pylint:disable=method-hidden
748785

749786
return (read_key, read_contents)
750787

751-
def clear(self):
788+
def clear(self) -> None:
752789
_PersistentDictBase.clear(self)
753790
self._fetch.cache_clear()
754791

755792

756-
class PersistentDict(_PersistentDictBase):
793+
class PersistentDict(_PersistentDictBase[K, V]):
757794
"""A concurrent disk-backed dictionary.
758795
759796
.. automethod:: __init__
@@ -766,15 +803,22 @@ class PersistentDict(_PersistentDictBase):
766803
.. automethod:: fetch
767804
.. automethod:: remove
768805
"""
769-
def __init__(self, identifier, key_builder=None, container_dir=None):
806+
def __init__(self,
807+
identifier: str,
808+
key_builder: Optional[KeyBuilder] = None,
809+
container_dir: Optional[str] = None) -> None:
770810
"""
771811
:arg identifier: a file-name-compatible string identifying this
772812
dictionary
773813
:arg key_builder: a subclass of :class:`KeyBuilder`
814+
:arg container_dir: the directory in which to store this
815+
dictionary. If ``None``, the default cache directory from
816+
:func:`platformdirs.user_cache_dir` is used
774817
"""
775818
_PersistentDictBase.__init__(self, identifier, key_builder, container_dir)
776819

777-
def store(self, key, value, _skip_if_present=False, _stacklevel=0):
820+
def store(self, key: K, value: V, _skip_if_present: bool = False,
821+
_stacklevel: int = 0) -> None:
778822
hexdigest_key = self.key_builder(key)
779823

780824
cleanup_m = CleanupManager()
@@ -807,7 +851,7 @@ def store(self, key, value, _skip_if_present=False, _stacklevel=0):
807851
finally:
808852
cleanup_m.clean_up()
809853

810-
def fetch(self, key, _stacklevel=0):
854+
def fetch(self, key: K, _stacklevel: int = 0) -> V:
811855
hexdigest_key = self.key_builder(key)
812856
item_dir = self._item_dir(hexdigest_key)
813857

@@ -871,7 +915,8 @@ def fetch(self, key, _stacklevel=0):
871915
finally:
872916
cleanup_m.clean_up()
873917

874-
def remove(self, key, _stacklevel=0):
918+
def remove(self, key: K, _stacklevel: int = 0) -> None:
919+
"""Remove the entry associated with *key* from the dictionary."""
875920
hexdigest_key = self.key_builder(key)
876921

877922
item_dir = self._item_dir(hexdigest_key)
@@ -913,7 +958,8 @@ def remove(self, key, _stacklevel=0):
913958
finally:
914959
cleanup_m.clean_up()
915960

916-
def __delitem__(self, key):
961+
def __delitem__(self, key: K) -> None:
962+
"""Remove the entry associated with *key* from the dictionary."""
917963
self.remove(key, _stacklevel=1)
918964

919965
# }}}

0 commit comments

Comments
 (0)