Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ answer newbie questions, and generally made Django that much better:
Jonas Lundberg <jonas.lundberg@gmail.com>
Jonathan Davis <jonathandavis47780@gmail.com>
Jonatas C. D. <jonatas.cd@gmail.com>
Jonathan Biemond <jonathan.biemond@gmail.com>
Jonathan Buchanan <jonathan.buchanan@gmail.com>
Jonathan Daugherty (cygnus) <http://www.cprogrammer.org/>
Jonathan Feignberg <jdf@pobox.com>
Expand Down
30 changes: 29 additions & 1 deletion django/core/cache/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 []
Expand All @@ -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)

Expand Down Expand Up @@ -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

Expand All @@ -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]")
Expand Down
15 changes: 6 additions & 9 deletions django/db/backends/base/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion django/db/models/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def __init__(
self,
*expressions,
fields=(),
name=None,
name,
condition=None,
deferrable=None,
include=None,
Expand Down
8 changes: 4 additions & 4 deletions django/test/testcases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
37 changes: 37 additions & 0 deletions docs/howto/deployment/asgi/granian.txt
Original file line number Diff line number Diff line change
@@ -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 <Granian_>`_.
1 change: 1 addition & 0 deletions docs/howto/deployment/asgi/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Django includes getting-started documentation for the following ASGI servers:
:maxdepth: 1

daphne
granian
hypercorn
uvicorn

Expand Down
37 changes: 37 additions & 0 deletions docs/howto/deployment/wsgi/granian.txt
Original file line number Diff line number Diff line change
@@ -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 <Granian_>`_.
1 change: 1 addition & 0 deletions docs/howto/deployment/wsgi/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Django includes getting-started documentation for the following WSGI servers:
.. toctree::
:maxdepth: 1

granian
gunicorn
uwsgi
modwsgi
Expand Down
2 changes: 1 addition & 1 deletion docs/ref/models/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ gis
GiST
Googol
Greenhill
Granian
gunicorn
GZip
gzipped
Expand Down
86 changes: 85 additions & 1 deletion tests/cache/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tests/constraints/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading