Skip to content

yggdrasil.dataclasses.singleton

singleton

Hash-keyed singleton base class — process-wide instance cache.

Pairs __new__ with an :class:ExpiringDict so two constructor calls that produce the same hashable key collapse to one instance. The singleton key is whatever :meth:Singleton._singleton_key projects out of the constructor arguments — by default (cls, args, sorted kwargs items), which works for any constructor whose arguments are hashable.

Subclasses with non-trivial constructors override :meth:_singleton_key to:

  • drop fields that don't participate in instance identity;
  • normalize unhashable inputs (dictfrozenset of items, listtuple);
  • canonicalize semantically equivalent forms (e.g. trailing slash on a URL) so different spellings of the same identity collapse.

__init__ must be idempotent: Python re-invokes it on every constructor call, even when __new__ returns the cached instance, so subclasses guard with if getattr(self, "_initialized", False): return to avoid clobbering live state on the second pass.

Pickling is wired through :meth:__getstate__ / :meth:__setstate__: attribute names listed in _TRANSIENT_STATE_ATTRS are excluded from the payload (live SDK clients, sockets, lazy service caches), and __setstate__ short-circuits when the receiver is already initialized so unpickling collapses to the live in-process singleton.

Singleton

Base class that caches one instance per hashable constructor key.

The cache is shared across every subclass (the default _singleton_key includes cls so different subclasses can coexist in one dict). A subclass that wants a private cache re-declares its own _INSTANCES ClassVar.

No mutex anywhere — :meth:__new__, :meth:to_singleton, and :meth:invalidate_singleton all ride :class:ExpiringDict's lockless GIL-atomic primitives (get_or_set for atomic check-and-insert, pop for atomic identity-guarded remove). Cannot deadlock by construction.

to_singleton

to_singleton(ttl: Any = ...) -> 'Singleton'

Promote this instance into the per-class _INSTANCES cache.

Hot listing paths (iterdir / _ls / glob) build children with singleton_ttl=False so the bounded cache doesn't fill up with thousands of short-lived entries. When a caller decides one of those children is worth keeping around (handing it to a long-running worker, returning it from an API), :meth:to_singleton registers self into the cache so the next constructor call with the same key collapses to the same instance.

ttl defaults to the subclass's _SINGLETON_TTL (... = no caching, None = process lifetime, or a seconds count). When a different instance is already cached under this key, that pre-existing one wins and is returned unchanged — the cache is the source of truth.

invalidate_singleton

invalidate_singleton(remove_global: bool = True) -> None

Pop self from the per-class _INSTANCES cache.

Mutating ops on a Singleton-cached object (writes, deletes, schema invalidations on a Databricks table, put_object on an :class:S3Path) want to make sure the next caller asking for the same key gets a fresh build rather than collapsing onto this stale handle — that's what remove_global=True (the default) does. The pop is :meth:identity-guarded: only an entry that still points at self is removed, so a concurrent re-construction that already raced past this thread is left alone.

remove_global=False is a no-op. The keyword exists so subclass invalidators (invalidate_singleton, _invalidate_entity_tag_cache, …) can offer the same switch without branching at the call site.