Source code for numbox.utils.digest

"""Content-addressed digest for cache keys and anchor identifiers.

``digest`` produces a short, stable hash that invalidates when the subject's
``repr``, the user functions, the resolved ``jit_options``, or the numba/numbox
versions change. Plain Python functions are fingerprinted with the shared
closure/global-aware walker (``numbox.utils.fingerprint._fingerprint_function``)
-- so two callbacks with identical source but different captured closure-cell or
referenced-global values key distinctly, which a bare code-object hash would
miss. Callables with no canonical fingerprint (a partial, a builtin, a callable
object, or a function closing over an un-canonicalizable value) fall back to
cloudpickle of the object/code, which also captures bound state. The SQLite UDAF
registration anchors are one consumer; any content-addressed cache that mixes a
type/identifier with user callbacks can reuse it.
"""
import hashlib

from types import FunctionType

import numba
from numba.core.serialize import cloudpickle

import numbox
from numbox.core.configurations import jit_options
from numba.core.dispatcher import Dispatcher
from numba.np.ufunc.dufunc import DUFunc

from numbox.utils.fingerprint import (
    _Unfingerprintable, _canon_value, _fingerprint_function, _fingerprint_function_best_effort,
)


[docs] def digest(subject, fns): """Content hash that invalidates when ``subject`` (by ``repr``), the user functions (closure/global/const-sensitive via the shared fingerprint walker, with a cloudpickle fallback), the resolved ``jit_options``, or the numba/numbox versions change.""" h = hashlib.sha256() h.update(repr(subject).encode("utf-8")) h.update(numba.__version__.encode("utf-8")) # numbox.__version__ is "" upstream (the package version derives from it via # pyproject's dynamic attr), so this fold is currently inert; it is kept so # digests auto-invalidate should numbox ever set a real __version__. h.update((numbox.__version__ or "").encode("utf-8")) # fold the resolved numbox-wide jit_options so flipping NUMBOX_JIT_OPTIONS # (e.g. cache off) re-keys the digest; numba's own cache also keys on flags. h.update(repr(sorted(jit_options.items())).encode("utf-8")) for fn in fns: if isinstance(fn, (Dispatcher, DUFunc)): # Route jitted callables through the shared value canonicalizer, # which folds targetoptions (and honours a @proxy's content-addressed # alias). The py_func shortcut below sees only the Python body, so # dispatchers over one body compiled with different jit flags -- # nogil, fastmath, error_model, boundscheck -- all collided. try: h.update(_canon_value(fn, set()).encode("utf-8")) continue except (_Unfingerprintable, RecursionError): # Body not canonicalizable; fold the flags on their own so they # still re-key, then fall through to the best-effort walker. h.update(repr(sorted((getattr(fn, "targetoptions", None) or {}).items())).encode("utf-8")) py = getattr(fn, "py_func", fn) if isinstance(py, FunctionType): try: h.update(_fingerprint_function(py, set()).encode("utf-8")) except (_Unfingerprintable, RecursionError): # An un-canonicalizable closure/global aborts the strict walker; # the best-effort walker still captures the constants, closure # cells, defaults and globals it can (substituting opaque # placeholders for the rest, and sorting sets so it is # PYTHONHASHSEED-stable). A bare __code__ cloudpickle here dropped # exactly the closure/default state that forced the fallback and # leaked str-set iteration order. h.update(_fingerprint_function_best_effort(py).encode("utf-8")) continue # Codeless callable (partial / builtin / callable object): cloudpickle the # object to capture bound state. (Only these remain on the cloudpickle # path, so its uuid4 nondeterminism for __main__ objects is confined here.) h.update(cloudpickle.dumps(py)) return h.hexdigest()[:16]