From b172cbdf33c3eaea6a5c4a53da2ed0e46306a625 Mon Sep 17 00:00:00 2001 From: Jonathan Biemond Date: Thu, 18 Dec 2025 14:43:59 +0100 Subject: [PATCH 1/4] Fixed #36808 -- Required name argument in UniqueConstraint signature. By trading ValueError for TypeError for omitted name arguments, we gain a little clarity. --- AUTHORS | 1 + django/db/models/constraints.py | 2 +- docs/ref/models/constraints.txt | 2 +- tests/constraints/tests.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5e7bca67f587..f6d4c5fe26cd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -551,6 +551,7 @@ answer newbie questions, and generally made Django that much better: Jonas Lundberg Jonathan Davis Jonatas C. D. + Jonathan Biemond Jonathan Buchanan Jonathan Daugherty (cygnus) Jonathan Feignberg diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 428631997806..7dfeb3b64983 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -265,7 +265,7 @@ def __init__( self, *expressions, fields=(), - name=None, + name, condition=None, deferrable=None, include=None, diff --git a/docs/ref/models/constraints.txt b/docs/ref/models/constraints.txt index 9a51c5c7a2e6..36a79e1c0b29 100644 --- a/docs/ref/models/constraints.txt +++ b/docs/ref/models/constraints.txt @@ -130,7 +130,7 @@ ensures the age field is never less than 18. ``UniqueConstraint`` ==================== -.. class:: UniqueConstraint(*expressions, fields=(), name=None, condition=None, deferrable=None, include=None, opclasses=(), nulls_distinct=None, violation_error_code=None, violation_error_message=None) +.. class:: UniqueConstraint(*expressions, fields=(), name, condition=None, deferrable=None, include=None, opclasses=(), nulls_distinct=None, violation_error_code=None, violation_error_message=None) Creates a unique constraint in the database. diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index f9881215284f..39b170125f12 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -1462,7 +1462,7 @@ def test_expressions_with_opclasses(self): def test_requires_name(self): msg = "A unique constraint must be named." with self.assertRaisesMessage(ValueError, msg): - models.UniqueConstraint(fields=["field"]) + models.UniqueConstraint(fields=["field"], name="") def test_database_default(self): models.UniqueConstraint( From 60fecd1d445224fa00385f5f1fde75999da7bec8 Mon Sep 17 00:00:00 2001 From: JaeHyuck Sa Date: Fri, 19 Dec 2025 10:15:45 +0900 Subject: [PATCH 2/4] Fixed #36781 -- Added Granian to deployment documentation. Signed-off-by: JaeHyuck Sa --- docs/howto/deployment/asgi/granian.txt | 37 ++++++++++++++++++++++++++ docs/howto/deployment/asgi/index.txt | 1 + docs/howto/deployment/wsgi/granian.txt | 37 ++++++++++++++++++++++++++ docs/howto/deployment/wsgi/index.txt | 1 + docs/spelling_wordlist | 1 + 5 files changed, 77 insertions(+) create mode 100644 docs/howto/deployment/asgi/granian.txt create mode 100644 docs/howto/deployment/wsgi/granian.txt diff --git a/docs/howto/deployment/asgi/granian.txt b/docs/howto/deployment/asgi/granian.txt new file mode 100644 index 000000000000..e16e9d8103b9 --- /dev/null +++ b/docs/howto/deployment/asgi/granian.txt @@ -0,0 +1,37 @@ +============================== +How to use Django with Granian +============================== + +Granian_ is a Rust-based high-performance HTTP server for Python applications, +supporting ASGI and WSGI interfaces. + +.. _Granian: https://github.com/emmett-framework/granian + +Installing Granian +================== + +You can install Granian with ``pip``: + +.. code-block:: shell + + python -m pip install granian + +Running Django in Granian +========================= + +When Granian is installed, a ``granian`` command is available which runs ASGI +applications. Granian needs to be called with the location of a module +containing an ASGI application object, followed by what the application is +called (separated by a colon), using the ``--interface asgi`` option. + +For a typical Django project, invoking Granian would look like: + +.. code-block:: shell + + granian myproject.asgi:application --interface asgi + +This will start one process listening on ``127.0.0.1:8000``. It requires that +your project be on the Python path; to ensure that, run this command from the +same directory as your ``manage.py`` file. + +For more advanced usage, please read the `Granian documentation `_. diff --git a/docs/howto/deployment/asgi/index.txt b/docs/howto/deployment/asgi/index.txt index 6015554350cf..9bead6cc3bcd 100644 --- a/docs/howto/deployment/asgi/index.txt +++ b/docs/howto/deployment/asgi/index.txt @@ -17,6 +17,7 @@ Django includes getting-started documentation for the following ASGI servers: :maxdepth: 1 daphne + granian hypercorn uvicorn diff --git a/docs/howto/deployment/wsgi/granian.txt b/docs/howto/deployment/wsgi/granian.txt new file mode 100644 index 000000000000..2252ed01092c --- /dev/null +++ b/docs/howto/deployment/wsgi/granian.txt @@ -0,0 +1,37 @@ +============================== +How to use Django with Granian +============================== + +Granian_ is a Rust-based high-performance HTTP server for Python applications, +supporting ASGI and WSGI interfaces. + +.. _Granian: https://github.com/emmett-framework/granian + +Installing Granian +================== + +You can install Granian with ``pip``: + +.. code-block:: shell + + python -m pip install granian + +Running Django in Granian +========================= + +When Granian is installed, a ``granian`` command is available which runs WSGI +applications. Granian needs to be called with the location of a module +containing a WSGI application object named ``application``, using the +``--interface wsgi`` option. + +For a typical Django project, invoking Granian would look like: + +.. code-block:: shell + + granian myproject.wsgi:application --interface wsgi + +This will start one process listening on ``127.0.0.1:8000``. It requires that +your project be on the Python path; to ensure that, run this command from the +same directory as your ``manage.py`` file. + +For more advanced usage, please read the `Granian documentation `_. diff --git a/docs/howto/deployment/wsgi/index.txt b/docs/howto/deployment/wsgi/index.txt index 0d3b0cd22035..5284400fbdaf 100644 --- a/docs/howto/deployment/wsgi/index.txt +++ b/docs/howto/deployment/wsgi/index.txt @@ -16,6 +16,7 @@ Django includes getting-started documentation for the following WSGI servers: .. toctree:: :maxdepth: 1 + granian gunicorn uwsgi modwsgi diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index b35c94fc1019..7e1b147d018c 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -191,6 +191,7 @@ gis GiST Googol Greenhill +Granian gunicorn GZip gzipped From 7a2f35b1b755b20a1e4d290a88bf059e6897f798 Mon Sep 17 00:00:00 2001 From: Krishnaprasad MG Date: Mon, 15 Dec 2025 15:39:06 +0100 Subject: [PATCH 3/4] Fixed #36487 -- Fixed logger error message with partial callbacks. --- django/db/backends/base/base.py | 15 ++++------ django/test/testcases.py | 8 +++--- tests/test_utils/tests.py | 28 +++++++++++++++++++ tests/transaction_hooks/tests.py | 47 ++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py index 3f3d29874a4b..23015a57a3eb 100644 --- a/django/db/backends/base/base.py +++ b/django/db/backends/base/base.py @@ -741,11 +741,8 @@ def on_commit(self, func, robust=False): try: func() except Exception as e: - logger.error( - f"Error calling {func.__qualname__} in on_commit() (%s).", - e, - exc_info=True, - ) + name = getattr(func, "__qualname__", func) + logger.exception("Error calling %s in on_commit() (%s).", name, e) else: func() @@ -759,11 +756,11 @@ def run_and_clear_commit_hooks(self): try: func() except Exception as e: - logger.error( - f"Error calling {func.__qualname__} in on_commit() during " - f"transaction (%s).", + name = getattr(func, "__qualname__", func) + logger.exception( + "Error calling %s in on_commit() during transaction (%s).", + name, e, - exc_info=True, ) else: func() diff --git a/django/test/testcases.py b/django/test/testcases.py index c587f770a6a9..c56fa52806ea 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -1523,11 +1523,11 @@ def captureOnCommitCallbacks(cls, *, using=DEFAULT_DB_ALIAS, execute=False): try: callback() except Exception as e: - logger.error( - f"Error calling {callback.__qualname__} in " - f"on_commit() (%s).", + name = getattr(callback, "__qualname__", callback) + logger.exception( + "Error calling %s in on_commit() (%s).", + name, e, - exc_info=True, ) else: callback() diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 70cca3d4418d..92f13ab81c6f 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -4,6 +4,7 @@ import traceback import unittest import warnings +from functools import partial from io import StringIO from unittest import mock @@ -2145,6 +2146,33 @@ def hook(): self.assertIsInstance(raised_exception, MyException) self.assertEqual(str(raised_exception), "robust callback") + def test_execute_robust_with_callback_as_partial(self): + class MyException(Exception): + pass + + def hook(): + self.callback_called = True + raise MyException("robust callback") + + hook_partial = partial(hook) + + with self.assertLogs("django.test", "ERROR") as cm: + with self.captureOnCommitCallbacks(execute=True) as callbacks: + transaction.on_commit(hook_partial, robust=True) + + self.assertEqual(len(callbacks), 1) + self.assertIs(self.callback_called, True) + + log_record = cm.records[0] + self.assertEqual( + log_record.getMessage(), + f"Error calling {hook_partial} in on_commit() (robust callback).", + ) + self.assertIsNotNone(log_record.exc_info) + raised_exception = log_record.exc_info[1] + self.assertIsInstance(raised_exception, MyException) + self.assertEqual(str(raised_exception), "robust callback") + class DisallowedDatabaseQueriesTests(SimpleTestCase): def test_disallowed_database_connections(self): diff --git a/tests/transaction_hooks/tests.py b/tests/transaction_hooks/tests.py index 938e92575f0c..539016507260 100644 --- a/tests/transaction_hooks/tests.py +++ b/tests/transaction_hooks/tests.py @@ -1,3 +1,5 @@ +from functools import partial + from django.db import connection, transaction from django.test import TransactionTestCase, skipUnlessDBFeature @@ -63,6 +65,28 @@ def robust_callback(): self.assertIsInstance(raised_exception, ForcedError) self.assertEqual(str(raised_exception), "robust callback") + def test_robust_if_no_transaction_with_callback_as_partial(self): + def robust_callback(): + raise ForcedError("robust callback") + + robust_callback_partial = partial(robust_callback) + + with self.assertLogs("django.db.backends.base", "ERROR") as cm: + transaction.on_commit(robust_callback_partial, robust=True) + self.do(1) + + self.assertDone([1]) + log_record = cm.records[0] + self.assertEqual( + log_record.getMessage(), + f"Error calling {robust_callback_partial} " + f"in on_commit() (robust callback).", + ) + self.assertIsNotNone(log_record.exc_info) + raised_exception = log_record.exc_info[1] + self.assertIsInstance(raised_exception, ForcedError) + self.assertEqual(str(raised_exception), "robust callback") + def test_robust_transaction(self): def robust_callback(): raise ForcedError("robust callback") @@ -84,6 +108,29 @@ def robust_callback(): self.assertIsInstance(raised_exception, ForcedError) self.assertEqual(str(raised_exception), "robust callback") + def test_robust_transaction_with_callback_as_partial(self): + def robust_callback(): + raise ForcedError("robust callback") + + robust_callback_partial = partial(robust_callback) + + with self.assertLogs("django.db.backends", "ERROR") as cm: + with transaction.atomic(): + transaction.on_commit(robust_callback_partial, robust=True) + self.do(1) + + self.assertDone([1]) + log_record = cm.records[0] + self.assertEqual( + log_record.getMessage(), + f"Error calling {robust_callback_partial} in on_commit() during " + "transaction (robust callback).", + ) + self.assertIsNotNone(log_record.exc_info) + raised_exception = log_record.exc_info[1] + self.assertIsInstance(raised_exception, ForcedError) + self.assertEqual(str(raised_exception), "robust callback") + def test_delays_execution_until_after_transaction_commit(self): with transaction.atomic(): self.do(1) From 948f1bf5a1c1c93ee9c6f4baa2fd990e31361f81 Mon Sep 17 00:00:00 2001 From: eevelweezel Date: Thu, 18 Dec 2025 16:17:14 -0600 Subject: [PATCH 4/4] Fixed #36590 -- Made async cache methods use specialized sync versions if available. Thanks Simon Charette, Sarah Boyce, and Jacob Walls for reviews. --- django/core/cache/backends/base.py | 30 ++++++++++- tests/cache/tests.py | 86 +++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index f1b731aee1f3..67f31d9ac873 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -208,6 +208,10 @@ def get_many(self, keys, version=None): async def aget_many(self, keys, version=None): """See get_many().""" + if self.get_many.__func__ is not BaseCache.get_many: + return await sync_to_async(self.get_many, thread_sensitive=True)( + keys, version=version + ) d = {} for k in keys: val = await self.aget(k, self._missing_key, version=version) @@ -236,6 +240,10 @@ def get_or_set(self, key, default, timeout=DEFAULT_TIMEOUT, version=None): async def aget_or_set(self, key, default, timeout=DEFAULT_TIMEOUT, version=None): """See get_or_set().""" + if self.get_or_set.__func__ is not BaseCache.get_or_set: + return await sync_to_async(self.get_or_set, thread_sensitive=True)( + key, default, timeout=timeout, version=version + ) val = await self.aget(key, self._missing_key, version=version) if val is self._missing_key: if callable(default): @@ -255,6 +263,10 @@ def has_key(self, key, version=None): ) async def ahas_key(self, key, version=None): + if self.has_key.__func__ is not BaseCache.has_key: + return await sync_to_async(self.has_key, thread_sensitive=True)( + key, version=version + ) return ( await self.aget(key, self._missing_key, version=version) is not self._missing_key @@ -274,6 +286,10 @@ def incr(self, key, delta=1, version=None): async def aincr(self, key, delta=1, version=None): """See incr().""" + if self.incr.__func__ is not BaseCache.incr: + return await sync_to_async(self.incr, thread_sensitive=True)( + key, delta=delta, version=version + ) value = await self.aget(key, self._missing_key, version=version) if value is self._missing_key: raise ValueError("Key '%s' not found" % key) @@ -317,6 +333,10 @@ def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): return [] async def aset_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): + if self.set_many.__func__ is not BaseCache.set_many: + return await sync_to_async(self.set_many, thread_sensitive=True)( + data, timeout=timeout, version=version + ) for key, value in data.items(): await self.aset(key, value, timeout=timeout, version=version) return [] @@ -331,6 +351,10 @@ def delete_many(self, keys, version=None): self.delete(key, version=version) async def adelete_many(self, keys, version=None): + if self.delete_many.__func__ is not BaseCache.delete_many: + return await sync_to_async(self.delete_many, thread_sensitive=True)( + keys, version=version + ) for key in keys: await self.adelete(key, version=version) @@ -361,6 +385,10 @@ def incr_version(self, key, delta=1, version=None): async def aincr_version(self, key, delta=1, version=None): """See incr_version().""" + if self.incr_version.__func__ is not BaseCache.incr_version: + return await sync_to_async(self.incr_version, thread_sensitive=True)( + key, delta=delta, version=version + ) if version is None: version = self.version @@ -387,7 +415,7 @@ def close(self, **kwargs): pass async def aclose(self, **kwargs): - pass + return await sync_to_async(self.close, thread_sensitive=True)(**kwargs) memcached_error_chars_re = _lazy_re_compile(r"[\x00-\x20\x7f]") diff --git a/tests/cache/tests.py b/tests/cache/tests.py index c4cf0a84e3a9..db5df2070124 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -25,7 +25,7 @@ cache, caches, ) -from django.core.cache.backends.base import InvalidCacheBackendError +from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError from django.core.cache.backends.redis import RedisCacheClient from django.core.cache.utils import make_template_fragment_key from django.db import close_old_connections, connection, connections @@ -1165,6 +1165,90 @@ def test_get_or_set_racing(self): cache_add.return_value = False self.assertEqual(cache.get_or_set("key", "default"), "default") + async def test_get_many_async_uses_specialized_implementation(self): + if ( + cache.get_many.__func__ is not BaseCache.get_many + and cache.aget_many.__func__ is BaseCache.aget_many + ): + with mock.patch.object(cache, "get_many") as mocked: + mocked.__func__ = lambda x: x + await cache.aget_many({}) + mocked.assert_called_once() + + async def test_set_many_async_uses_specialized_implementation(self): + if ( + cache.set_many.__func__ is not BaseCache.set_many + and cache.aset_many.__func__ is BaseCache.aset_many + ): + with mock.patch.object(cache, "set_many") as mocked: + mocked.__func__ = lambda x: x + await cache.aset_many({}) + mocked.assert_called_once() + + async def test_delete_many_async_uses_specialized_implementation(self): + if ( + cache.delete_many.__func__ is not BaseCache.delete_many + and cache.adelete_many.__func__ is BaseCache.adelete_many + ): + with mock.patch.object(cache, "delete_many") as mocked: + mocked.__func__ = lambda x: x + await cache.adelete_many({}) + mocked.assert_called_once() + + async def test_get_or_set_async_uses_specialized_implementation(self): + await cache.aset("key", "value") + if ( + cache.get_or_set.__func__ is not BaseCache.get_or_set + and cache.aget_or_set.__func__ is BaseCache.aget_or_set + ): + with mock.patch.object(cache, "get_or_set") as mocked: + mocked.__func__ = lambda x: x + await cache.aget_or_set("key") + mocked.assert_called_once() + + async def test_has_key_async_uses_specialized_implementation(self): + await cache.aset("key", "value") + if ( + cache.has_key.__func__ is not BaseCache.has_key + and cache.ahas_key.__func__ is BaseCache.ahas_key + ): + with mock.patch.object(cache, "has_key") as mocked: + mocked.__func__ = lambda x: x + await cache.ahas_key("key") + mocked.assert_called_once() + + async def test_incr_async_uses_specialized_implementation(self): + await cache.aset("key", 1) + if ( + cache.incr.__func__ is not BaseCache.incr + and cache.aincr.__func__ is BaseCache.aincr + ): + with mock.patch.object(cache, "incr") as mocked: + mocked.__func__ = lambda x: x + await cache.aincr("key") + mocked.assert_called_once() + + async def test_incr_version_async_uses_specialized_implementation(self): + await cache.aset("key", "value", version=1) + if ( + cache.incr_version.__func__ is not BaseCache.incr_version + and cache.aincr_version.__func__ is BaseCache.aincr_version + ): + with mock.patch.object(cache, "incr_version") as mocked: + mocked.__func__ = lambda x: x + await cache.aincr_version("key") + mocked.assert_called_once() + + async def test_close_async_uses_specialized_implementation(self): + if ( + cache.close.__func__ is not BaseCache.close + and cache.aclose.__func__ is BaseCache.aclose + ): + with mock.patch.object(cache, "close") as mocked: + mocked.__func__ = lambda x: x + await cache.aclose() + mocked.assert_called_once() + @override_settings( CACHES=caches_setting_for_tests(