Source code for gain.genomic_resources.memo

"""Process-wide memos of objects built from a resource or a local file.

Gene models, gene scores, gene set collections and liftover chains are
expensive to build and are shared by every caller in a process. Each
factory keeps one memo; a resource-built object is keyed on
``GenomicResource.get_memo_key()``, a file-built one on the file name and
``config_memo_key()`` of the synthetic config it builds. The rule, and the
two resource-keyed caches that deliberately do not follow it, are recorded
in ADR 0036 (``docs/adr/``).
"""
from __future__ import annotations

from collections.abc import Callable, Hashable, Mapping
from operator import itemgetter
from threading import Lock
from typing import Any


def _canonical_config(value: Any) -> Any:
    """Return ``value`` with the order it was written in taken out.

    A mapping becomes a tuple of pairs sorted by key, so two configs
    differing only in the order their keys were written canonicalise
    alike; a sequence keeps its order. Keys are stringified first, a bare
    number being a legal yaml key that cannot be sorted against a string
    one, and the sort looks at the key alone, so values of unrelated
    types are never compared. Leaves are returned untouched: the caller
    ``repr``\\ s the result, so a value with no JSON spelling -- a bare
    date -- needs no handling of its own.

    The result is not injective. Two keys that differ only until they are
    stringified collapse together, as do a mapping and a sequence of
    pairs that canonicalise alike; neither is reachable from a parsed
    ``genomic_resource.yaml``. A config holding itself recurses until the
    stack runs out.
    """
    if isinstance(value, Mapping):
        return tuple(sorted(
            ((str(key), _canonical_config(val)) for key, val in value.items()),
            key=itemgetter(0)))
    if isinstance(value, (list, tuple)):
        return tuple(_canonical_config(val) for val in value)
    return value


[docs] def config_memo_key(config: Any) -> str: """Return the memo-key spelling of a resource config. Two configs differing only in the order their keys were written get the same key. Any value a parsed yaml can hold is accepted, including a bare date and a number used as a key beside string ones. """ return repr(_canonical_config(config))
[docs] class Memo[K: Hashable, V]: """A thread-safe memo from a key to a built object. ``get_or_build`` returns the object stored under a key, building and storing it first on a miss. The build runs under the memo's lock, so two callers racing on one key build it once and both receive the very same object; a builder must therefore not call back into the same memo. The memo never evicts on its own: objects are held by strong reference until ``clear`` drops them all, or ``discard`` drops the ones whose key a predicate accepts. """ def __init__(self) -> None: self._built: dict[K, V] = {} self._lock = Lock()
[docs] def get_or_build(self, key: K, build: Callable[[], V]) -> V: """Return the object memoised under ``key``, building it if absent. A builder that raises leaves nothing stored under ``key``. """ with self._lock: if key in self._built: return self._built[key] built = build() self._built[key] = built return built
[docs] def clear(self) -> None: """Drop every memoised object.""" with self._lock: self._built.clear()
[docs] def discard(self, doomed: Callable[[K], bool]) -> int: """Drop every memoised object whose key ``doomed`` accepts. Returns ------- int The number of objects dropped. """ with self._lock: keys = [key for key in self._built if doomed(key)] for key in keys: del self._built[key] return len(keys)
def __len__(self) -> int: return len(self._built)