Skip to content

Caching

Cached singleton-instance machinery for DotEnvConfig subclasses.

caching

Cached singleton-instance machinery for DotEnvConfig subclasses.

Provides the internal implementation behind DotEnvConfig.cached(), DotEnvConfig.reset_cached(), and DotEnvConfig.cached_override(). The public API lives on DotEnvConfig itself as thin classmethod wrappers that preserve concrete-subclass typing (Self); this module operates on plain type[DotEnvConfig] / DotEnvConfig and is not part of the package's public API.

The cached instance is stored as a private class attribute (_cached_instance) on each config subclass's own __dict__ rather than in a module-level registry. This ties the cache lifetime to the class object: when nothing else references the class, both the class and its cached instance become collectible together.

Thread safety

A module-level threading.RLock guards the double-checked-locking initialization path and the save/restore operations in begin_override / end_override. A reentrant lock (not a plain Lock) is required because the lock is held across cls.load(): post_load() and field validator hooks may legitimately touch the cache for other classes, and a non-reentrant lock would self-deadlock that thread. With a single module-level lock no cross-thread circular wait is possible — only one thread holds the lock and the holder always proceeds — so same-thread nesting is the only reentrancy case, and the RLock permits it.

Same-class operations from within that class's own in-flight load remain invalid: cached() cannot return an instance that does not exist yet (the nested call would see a cold cache and recurse into load() without bound), and reset_cached() / cached_override() would be silently overwritten when the in-flight load installs its instance. A threading.local set tracks classes currently loading on each thread; all three entry points raise RuntimeError in that case. A circular cross-class chain (A's hook loads B, B's hook loads A) collapses back onto the first class and is likewise reported as a same-class RuntimeError.

One residual hazard sits outside this design: the lock is held for the duration of a load, so a hook must not block on another thread (e.g. thread.join()) that touches the cache — the joining thread holds the lock while the joined thread waits for it, deadlocking both.