Source code for qs_codec.models.weak_wrapper

"""Weakly wrap *any* object with identity equality and deep content hashing."""

import reprlib
import typing as t
from threading import RLock
from weakref import ReferenceType, WeakValueDictionary, ref


__all__ = ["WeakWrapper", "_proxy_cache"]


# Exported for tests
_proxy_cache: "WeakValueDictionary[int, _Proxy]" = WeakValueDictionary()
_proxy_cache_lock = RLock()


class _Proxy:
    """Container for the original object.

    NOTE: Proxies must be weak-referenceable because the cache holds them in a WeakValueDictionary. That requires "__weakref__" in __slots__.
    """

    __slots__ = ("value", "__weakref__")

    def __init__(self, value: t.Any) -> None:
        """Strong ref to the value so hash/equality can access it while a wrapper keeps this proxy alive."""
        self.value = value


def _get_proxy(value: t.Any) -> "_Proxy":
    """Return a per-object proxy, cached by id(value)."""
    key = id(value)
    with _proxy_cache_lock:
        proxy = _proxy_cache.get(key)
        if proxy is None:
            proxy = _Proxy(value)
            _proxy_cache[key] = proxy
        return proxy


def _deep_hash(
    obj: t.Any,
    _seen: t.Optional[set[int]] = None,
    _depth: int = 0,
) -> int:
    """Deterministic deep hash with cycle & depth protection.

    - Raises ValueError("Circular reference detected") on cycles.
    - Raises RecursionError when nesting exceeds 400.
    - Produces equal hashes for equal-by-contents containers.
    """
    if _depth > 400:
        raise RecursionError("Maximum hashing depth exceeded")

    if _seen is None:
        _seen = set()

    # Track only containers by identity for cycle detection
    def _enter(o: t.Any) -> int:
        oid = id(o)
        if oid in _seen:
            raise ValueError("Circular reference detected")
        _seen.add(oid)
        return oid

    def _leave(oid: int) -> None:
        _seen.remove(oid)

    if isinstance(obj, dict):
        oid = _enter(obj)
        try:
            # Compute key/value deep hashes once and sort pairs for determinism
            pairs = [(_deep_hash(k, _seen, _depth + 1), _deep_hash(v, _seen, _depth + 1)) for k, v in obj.items()]
            pairs.sort()
            kv_hashes = tuple(pairs)
            return hash(("dict", kv_hashes))
        finally:
            _leave(oid)

    if isinstance(obj, (list, tuple)):
        oid = _enter(obj)
        try:
            elem_hashes = tuple(_deep_hash(x, _seen, _depth + 1) for x in obj)
            tag = "list" if isinstance(obj, list) else "tuple"
            return hash((tag, elem_hashes))
        finally:
            _leave(oid)

    if isinstance(obj, set):
        oid = _enter(obj)
        try:
            set_hashes = tuple(sorted(_deep_hash(x, _seen, _depth + 1) for x in obj))
            return hash(("set", set_hashes))
        finally:
            _leave(oid)

    # Fallback for scalars / unhashables
    try:
        return hash(obj)
    except TypeError:
        return hash(repr(obj))


[docs] class WeakWrapper: """Wrapper suitable for use as a WeakKeyDictionary key. - Holds a *strong* reference to the proxy (keeps proxy alive while wrapper exists). - Exposes a weakref to the proxy via `_wref` so tests can observe/force GC. - Equality is proxy identity; hash is a deep hash of the underlying value. """ __slots__ = ("_proxy", "_wref", "__weakref__") _proxy: _Proxy _wref: ReferenceType[_Proxy]
[docs] def __init__(self, value: t.Any) -> None: """Initialize the wrapper with a value.""" proxy = _get_proxy(value) # Strong edge: wrapper -> proxy object.__setattr__(self, "_proxy", proxy) # Weak edge so tests can observe GC of the proxy object.__setattr__(self, "_wref", ref(proxy))
@property def value(self) -> t.Any: """Guard with the weakref so tests can simulate GC by swapping _wref.""" if self._wref() is None: raise ReferenceError("Original object has been garbage-collected") return self._proxy.value def __repr__(self) -> str: """Return a string representation of the wrapper.""" if self._wref() is None: return "WeakWrapper(<gc'd>)" # Use reprlib to avoid excessive size and recursion issues without broad exception handling. return f"WeakWrapper({reprlib.repr(self._proxy.value)})" def __eq__(self, other: object) -> bool: """Check equality by comparing the proxy identity.""" if not isinstance(other, WeakWrapper): return NotImplemented # Same underlying object => same cached proxy instance return self._proxy is other._proxy def __hash__(self) -> int: """Return a deep hash of the wrapped value.""" # Uses your existing deep-hash helper (not shown here). return _deep_hash(self.value)