From 14547864eb60f229c821354c79d5c230f09537a6 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Thu, 27 Aug 2026 13:41:17 +0900 Subject: [PATCH 1/3] fix(django-cf): drop DJANGO_ALLOW_ASYNC_UNSAFE from django-cf backends --- packages/django-cf/django_cf/__init__.py | 4 -- .../django-cf/django_cf/db/base_engine.py | 41 +++++++++++++++++++ .../in_worker/worker/src/test_base_engine.py | 39 ++++++++++++++++++ .../tests/in_worker/worker/src/worker.py | 2 - 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/packages/django-cf/django_cf/__init__.py b/packages/django-cf/django_cf/__init__.py index 26f3d951..b93d0014 100644 --- a/packages/django-cf/django_cf/__init__.py +++ b/packages/django-cf/django_cf/__init__.py @@ -1,11 +1,7 @@ -import os - from workers import wsgi async def handle_wsgi(request, app, env=None): - os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "1") - return await wsgi.fetch(app, request, env) diff --git a/packages/django-cf/django_cf/db/base_engine.py b/packages/django-cf/django_cf/db/base_engine.py index d969dee6..bc715771 100644 --- a/packages/django-cf/django_cf/db/base_engine.py +++ b/packages/django-cf/django_cf/db/base_engine.py @@ -12,6 +12,7 @@ OperationalError, ProgrammingError, ) +from django.db.backends.base.base import BaseDatabaseWrapper from django.db.backends.sqlite3.base import DatabaseWrapper as SQLiteDatabaseWrapper from django.db.backends.sqlite3.client import DatabaseClient as SQLiteDatabaseClient from django.db.backends.sqlite3.creation import ( @@ -42,6 +43,7 @@ TruncYear, ) from django.db.models.sql.compiler import SQLCompiler +from django.utils import asyncio as _django_asyncio def replace_date_trunc_in_sql(sql): @@ -479,6 +481,35 @@ def _compile_date_trunc(self, func, **extra_context): return sql, params +def _async_unsafe_probe(): + pass + + +# Capture async_unsafe function so we don't accidentally unwrap something else +_ASYNC_UNSAFE_WRAPPER_CODE = _django_asyncio.async_unsafe(_async_unsafe_probe).__code__ + + +def _unwrap_async_unsafe(method): + """ + Unwrap Django's async_unsafe decorator to get the original method. + + This is needed because Django does not allow calling database operations + from within an async context, but Python workers always run in an async context. + + All the database backends that django-cf provides are async-safe so we need + to unwrap the async_unsafe decorator to allow calling database operations + from within an async context. + """ + method_code = getattr(method, "__code__", None) + is_async_unsafe = method_code is _ASYNC_UNSAFE_WRAPPER_CODE + wrapped = getattr(method, "__wrapped__", None) + + if is_async_unsafe: + return wrapped + + return method + + class CFDatabaseWrapper(SQLiteDatabaseWrapper): # this is defined in the class extending this one # vendor = "cloudflare_d1" @@ -496,6 +527,16 @@ class CFDatabaseWrapper(SQLiteDatabaseWrapper): transaction_modes = frozenset([]) + connect = _unwrap_async_unsafe(BaseDatabaseWrapper.connect) + ensure_connection = _unwrap_async_unsafe(BaseDatabaseWrapper.ensure_connection) + cursor = _unwrap_async_unsafe(BaseDatabaseWrapper.cursor) + commit = _unwrap_async_unsafe(BaseDatabaseWrapper.commit) + rollback = _unwrap_async_unsafe(BaseDatabaseWrapper.rollback) + savepoint = _unwrap_async_unsafe(BaseDatabaseWrapper.savepoint) + savepoint_rollback = _unwrap_async_unsafe(BaseDatabaseWrapper.savepoint_rollback) + savepoint_commit = _unwrap_async_unsafe(BaseDatabaseWrapper.savepoint_commit) + clean_savepoints = _unwrap_async_unsafe(BaseDatabaseWrapper.clean_savepoints) + def get_compiler(self, default_using=None, using=None, **kwargs): if using is None: using = default_using diff --git a/packages/django-cf/tests/in_worker/worker/src/test_base_engine.py b/packages/django-cf/tests/in_worker/worker/src/test_base_engine.py index cb01bbbb..a9fc0566 100644 --- a/packages/django-cf/tests/in_worker/worker/src/test_base_engine.py +++ b/packages/django-cf/tests/in_worker/worker/src/test_base_engine.py @@ -6,6 +6,10 @@ from unittest.mock import MagicMock, patch import pytest +from django.db.backends.base.base import BaseDatabaseWrapper +from django.utils.asyncio import async_unsafe + +from django_cf.db.base_engine import CFDatabaseWrapper, _unwrap_async_unsafe class TestCFResult: @@ -547,7 +551,42 @@ def test_bulk_insert_enabled(self): assert features.can_return_columns_from_insert is True +class TestUnwrapAsyncUnsafe: + def test_removes_async_unsafe_guard(self): + def target(): + return "called" + + guarded = async_unsafe(target) + + assert _unwrap_async_unsafe(guarded) is target + + def test_returns_undecorated_method_unchanged(self): + def target(): + return "called" + + assert _unwrap_async_unsafe(target) is target + + class TestCFDatabaseWrapper: + def test_connection_lifecycle_is_not_guarded_by_async_unsafe(self): + method_names = ( + "connect", + "ensure_connection", + "cursor", + "commit", + "rollback", + "savepoint", + "savepoint_rollback", + "savepoint_commit", + "clean_savepoints", + ) + + for method_name in method_names: + guarded_method = getattr(BaseDatabaseWrapper, method_name) + assert getattr(CFDatabaseWrapper, method_name) is _unwrap_async_unsafe( + guarded_method + ) + def test_get_database_version(self): from django_cf.db.base_engine import CFDatabaseWrapper diff --git a/packages/django-cf/tests/in_worker/worker/src/worker.py b/packages/django-cf/tests/in_worker/worker/src/worker.py index 50b8d2d2..3aeb561f 100644 --- a/packages/django-cf/tests/in_worker/worker/src/worker.py +++ b/packages/django-cf/tests/in_worker/worker/src/worker.py @@ -4,7 +4,6 @@ import contextlib import importlib.util import io -import os from pathlib import Path from urllib.parse import urlparse @@ -19,7 +18,6 @@ from workers import Response, WorkerEntrypoint BASE_DIR = Path(__file__).parent -os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true") async def _noop(*args): From 89afdb4b639df15abec7498a178a03bd525b06c5 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 28 Aug 2026 18:41:55 +0900 Subject: [PATCH 2/3] chore: extract out async_unsafe decorator into a helper file --- .../django-cf/django_cf/db/_async_unsafe.py | 33 +++++++++++++++++++ .../django-cf/django_cf/db/base_engine.py | 32 ++---------------- 2 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 packages/django-cf/django_cf/db/_async_unsafe.py diff --git a/packages/django-cf/django_cf/db/_async_unsafe.py b/packages/django-cf/django_cf/db/_async_unsafe.py new file mode 100644 index 00000000..318c040f --- /dev/null +++ b/packages/django-cf/django_cf/db/_async_unsafe.py @@ -0,0 +1,33 @@ +"""Helper for removing Django's ``async_unsafe`` guard from backend methods. +""" + +from django.utils import asyncio as _django_asyncio + + +def _async_unsafe_probe(): + pass + + +# Capture async_unsafe function so we don't accidentally unwrap something else +_ASYNC_UNSAFE_WRAPPER_CODE = _django_asyncio.async_unsafe(_async_unsafe_probe).__code__ + + +def _unwrap_async_unsafe(method): + """ + Unwrap Django's async_unsafe decorator to get the original method. + + This is needed because Django does not allow calling database operations + from within an async context, but Python workers always run in an async context. + + All the database backends that django-cf provides are async-safe so we need + to unwrap the async_unsafe decorator to allow calling database operations + from within an async context. + """ + method_code = getattr(method, "__code__", None) + is_async_unsafe = method_code is _ASYNC_UNSAFE_WRAPPER_CODE + wrapped = getattr(method, "__wrapped__", None) + + if is_async_unsafe: + return wrapped + + return method diff --git a/packages/django-cf/django_cf/db/base_engine.py b/packages/django-cf/django_cf/db/base_engine.py index bc715771..2f927fe1 100644 --- a/packages/django-cf/django_cf/db/base_engine.py +++ b/packages/django-cf/django_cf/db/base_engine.py @@ -43,7 +43,8 @@ TruncYear, ) from django.db.models.sql.compiler import SQLCompiler -from django.utils import asyncio as _django_asyncio + +from ._async_unsafe import _unwrap_async_unsafe def replace_date_trunc_in_sql(sql): @@ -481,35 +482,6 @@ def _compile_date_trunc(self, func, **extra_context): return sql, params -def _async_unsafe_probe(): - pass - - -# Capture async_unsafe function so we don't accidentally unwrap something else -_ASYNC_UNSAFE_WRAPPER_CODE = _django_asyncio.async_unsafe(_async_unsafe_probe).__code__ - - -def _unwrap_async_unsafe(method): - """ - Unwrap Django's async_unsafe decorator to get the original method. - - This is needed because Django does not allow calling database operations - from within an async context, but Python workers always run in an async context. - - All the database backends that django-cf provides are async-safe so we need - to unwrap the async_unsafe decorator to allow calling database operations - from within an async context. - """ - method_code = getattr(method, "__code__", None) - is_async_unsafe = method_code is _ASYNC_UNSAFE_WRAPPER_CODE - wrapped = getattr(method, "__wrapped__", None) - - if is_async_unsafe: - return wrapped - - return method - - class CFDatabaseWrapper(SQLiteDatabaseWrapper): # this is defined in the class extending this one # vendor = "cloudflare_d1" From 74044d98b2ac0050558fbf7a10aaf15a13d392c6 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 28 Aug 2026 18:45:02 +0900 Subject: [PATCH 3/3] chore: lint --- packages/django-cf/django_cf/db/_async_unsafe.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/django-cf/django_cf/db/_async_unsafe.py b/packages/django-cf/django_cf/db/_async_unsafe.py index 318c040f..8c94f1d2 100644 --- a/packages/django-cf/django_cf/db/_async_unsafe.py +++ b/packages/django-cf/django_cf/db/_async_unsafe.py @@ -1,5 +1,4 @@ -"""Helper for removing Django's ``async_unsafe`` guard from backend methods. -""" +"""Helper for removing Django's ``async_unsafe`` guard from backend methods.""" from django.utils import asyncio as _django_asyncio