from contextlib import AbstractContextManager
from contextvars import ContextVar, Token
from dataclasses import dataclass
from typing import overload, Callable
from . import caches, config, metadata
[docs]
_CACHE: ContextVar[caches.BaseCache] = ContextVar("fleche.CACHE", default=config.load_cache_config())
[docs]
class _StickyContext:
"""Context manager for sticky ContextVar state.
The value is set immediately on construction; entering the ``with``-block is a
no-op, and exiting restores the previous value via the stored token.
In Python 3.14+, ``Token`` objects returned by ``ContextVar.set()`` support the
context manager protocol natively, making this class unnecessary. It serves as
a backport for earlier Python versions.
"""
[docs]
__slots__ = ("_var", "_token")
def __init__(self, var: ContextVar, token: Token) -> None:
[docs]
self._token = token
[docs]
def __enter__(self) -> None:
return None
[docs]
def __exit__(self, *args: object) -> None:
self._var.reset(self._token)
@overload
[docs]
def cache(new_cache: None = None, stack: bool = False) -> caches.BaseCache: ...
@overload
def cache(
new_cache: caches.BaseCache | str, stack: bool = False
) -> AbstractContextManager[None]: ...
def cache(
new_cache: caches.BaseCache | str | None = None, stack: bool = False
) -> "caches.BaseCache | AbstractContextManager[None]":
"""
Manages the active cache for Fleche.
If ``new_cache`` is ``None``, returns the currently active cache.
Otherwise, immediately sets ``new_cache`` as the active cache and returns a context manager.
When used in a ``with`` statement the previous cache is restored on exit; when the returned
context manager is discarded the new cache remains active (sticky behaviour).
Args:
new_cache: Cache object or named cache string to activate, or ``None`` to query.
stack: If ``True``, wrap ``new_cache`` in a :class:`.CacheStack` on top of the current cache.
Returns:
The current :class:`.BaseCache` when called without arguments, otherwise a
:class:`._StickyContext` context manager.
"""
if new_cache is None:
return _CACHE.get()
if isinstance(new_cache, str):
new_cache = config.load_cache_config(new_cache)
if not isinstance(new_cache, caches.BaseCache):
raise ValueError(new_cache)
if stack:
new_cache = _CACHE.get().push(new_cache)
token = _CACHE.set(new_cache)
return _StickyContext(_CACHE, token)
[docs]
def project(name):
"""A context manager to tag results with a project name.
Args:
name (str): The name of the project.
"""
return tags(project=name)
@dataclass(frozen=True, eq=True)
[docs]
class BoundWrapper:
"""Utility class that freezes global state for the cache and metadata config.
Essentially acts like an early binding closure.
This is intended to enable passing around fleche-decorated functions in pickled form by baking in the state into the
pickle on request."""
[docs]
cache: caches.BaseCache
@classmethod
[docs]
def bind(cls, func):
"""Bind cache and metadata state.
Returns a new callable that will behave always as if run under the context under which :meth:`.bind()` was
originally called.
Args:
func (callable): any callable; plain functions that only call fleche-wrapped ones are explicitly allowed
Returns:
:class:`.BoundWrapper`: instance with the bound cache and metadata state"""
return cls(func, _CACHE.get(), _METADATA.get())
[docs]
def __call__(self, *args, **kwargs):
token_cache = _CACHE.set(self.cache)
token_meta = _METADATA.set(self.meta)
try:
return self.func(*args, **kwargs)
finally:
_METADATA.reset(token_meta)
_CACHE.reset(token_cache)