diff --git a/CHANGELOG.md b/CHANGELOG.md index af0e891..5882242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ## Next Release #### Improvements + - Python: Confirm Python 3.12 support ([#572](https://github.com/jazzband/django-auditlog/pull/572)) +- feat: `thread.local` replaced with `ContextVar` to improve context managers in Django 4.2+ ## 3.0.0-beta.2 (2023-10-05) diff --git a/auditlog/context.py b/auditlog/context.py index 80d2fae..644c6ce 100644 --- a/auditlog/context.py +++ b/auditlog/context.py @@ -1,32 +1,33 @@ import contextlib -import threading import time +from contextvars import ContextVar from functools import partial +from django.contrib.auth import get_user_model from django.db.models.signals import pre_save from auditlog.models import LogEntry -threadlocal = threading.local() +auditlog_value = ContextVar("auditlog_value") +auditlog_disabled = ContextVar("auditlog_disabled", default=False) @contextlib.contextmanager def set_actor(actor, remote_addr=None): """Connect a signal receiver with current user attached.""" # Initialize thread local storage - threadlocal.auditlog = { + context_data = { "signal_duid": ("set_actor", time.time()), "remote_addr": remote_addr, } + auditlog_value.set(context_data) # Connect signal for automatic logging - set_actor = partial( - _set_actor, user=actor, signal_duid=threadlocal.auditlog["signal_duid"] - ) + set_actor = partial(_set_actor, user=actor, signal_duid=context_data["signal_duid"]) pre_save.connect( set_actor, sender=LogEntry, - dispatch_uid=threadlocal.auditlog["signal_duid"], + dispatch_uid=context_data["signal_duid"], weak=False, ) @@ -34,12 +35,11 @@ def set_actor(actor, remote_addr=None): yield finally: try: - auditlog = threadlocal.auditlog - except AttributeError: + auditlog = auditlog_value.get() + except LookupError: pass else: pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog["signal_duid"]) - del threadlocal.auditlog def _set_actor(user, sender, instance, signal_duid, **kwargs): @@ -48,14 +48,18 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs): This function becomes a valid signal receiver when it is curried with the actor and a dispatch id. """ try: - auditlog = threadlocal.auditlog - except AttributeError: + auditlog = auditlog_value.get() + except LookupError: pass else: if signal_duid != auditlog["signal_duid"]: return - - if sender == LogEntry and instance.actor is None: + auth_user_model = get_user_model() + if ( + sender == LogEntry + and isinstance(user, auth_user_model) + and instance.actor is None + ): instance.actor = user instance.remote_addr = auditlog["remote_addr"] @@ -63,11 +67,11 @@ def _set_actor(user, sender, instance, signal_duid, **kwargs): @contextlib.contextmanager def disable_auditlog(): - threadlocal.auditlog_disabled = True + token = auditlog_disabled.set(True) try: yield finally: try: - del threadlocal.auditlog_disabled - except AttributeError: + auditlog_disabled.reset(token) + except LookupError: pass diff --git a/auditlog/receivers.py b/auditlog/receivers.py index b135db0..5d10495 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -2,7 +2,7 @@ from functools import wraps from django.conf import settings -from auditlog.context import threadlocal +from auditlog.context import auditlog_disabled from auditlog.diff import model_instance_diff from auditlog.models import LogEntry from auditlog.signals import post_log, pre_log @@ -17,7 +17,11 @@ def check_disable(signal_handler): @wraps(signal_handler) def wrapper(*args, **kwargs): - if not getattr(threadlocal, "auditlog_disabled", False) and not ( + try: + auditlog_disabled_value = auditlog_disabled.get() + except LookupError: + auditlog_disabled_value = False + if not auditlog_disabled_value and not ( kwargs.get("raw") and settings.AUDITLOG_DISABLE_ON_RAW_SAVE ): signal_handler(*args, **kwargs)