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
4 changes: 2 additions & 2 deletions django/contrib/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import inspect
import re

from django.apps import apps as django_apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from django.utils.inspect import signature
from django.utils.module_loading import import_string
from django.views.decorators.debug import sensitive_variables

Expand Down Expand Up @@ -40,7 +40,7 @@ def get_backends():

def _get_compatible_backends(request, **credentials):
for backend, backend_path in _get_backends(return_tuples=True):
backend_signature = inspect.signature(backend.authenticate)
backend_signature = signature(backend.authenticate)
try:
backend_signature.bind(request, **credentials)
except TypeError:
Expand Down
11 changes: 7 additions & 4 deletions django/contrib/gis/geos/libgeos.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,15 @@ def load_geos():
# See the GEOS C API source code for more details on the library function
# calls: https://libgeos.org/doxygen/geos__c_8h_source.html
_lgeos = CDLL(lib_path)
# Here we set up the prototypes for the initGEOS_r and finishGEOS_r
# routines. These functions aren't actually called until they are
# Here we set up the prototypes for the GEOS_init_r and GEOS_finish_r
# routines, as well as the context handler setters.
# These functions aren't actually called until they are
# attached to a GEOS context handle -- this actually occurs in
# geos/prototypes/threadsafe.py.
_lgeos.initGEOS_r.restype = CONTEXT_PTR
_lgeos.finishGEOS_r.argtypes = [CONTEXT_PTR]
_lgeos.GEOS_init_r.restype = CONTEXT_PTR
_lgeos.GEOS_finish_r.argtypes = [CONTEXT_PTR]
_lgeos.GEOSContext_setErrorHandler_r.argtypes = [CONTEXT_PTR, ERRORFUNC]
_lgeos.GEOSContext_setNoticeHandler_r.argtypes = [CONTEXT_PTR, NOTICEFUNC]
# Set restype for compatibility across 32 and 64-bit platforms.
_lgeos.GEOSversion.restype = c_char_p
return _lgeos
Expand Down
8 changes: 4 additions & 4 deletions django/contrib/gis/geos/prototypes/threadsafe.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ class GEOSContextHandle(GEOSBase):
"""Represent a GEOS context handle."""

ptr_type = CONTEXT_PTR
destructor = lgeos.finishGEOS_r
destructor = lgeos.GEOS_finish_r

def __init__(self):
# Initializing the context handler for this thread with
# the notice and error handler.
self.ptr = lgeos.initGEOS_r(notice_h, error_h)
self.ptr = lgeos.GEOS_init_r()
lgeos.GEOSContext_setNoticeHandler_r(self.ptr, notice_h)
lgeos.GEOSContext_setErrorHandler_r(self.ptr, error_h)


# Defining a thread-local object and creating an instance
Expand Down
5 changes: 2 additions & 3 deletions django/core/checks/security/csrf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import inspect

from django.conf import settings
from django.core.checks import Error, Tags, Warning, register
from django.utils.inspect import signature

W003 = Warning(
"You don't appear to be using Django's built-in "
Expand Down Expand Up @@ -56,7 +55,7 @@ def check_csrf_failure_view(app_configs, **kwargs):
errors.append(Error(msg, id="security.E102"))
else:
try:
inspect.signature(view).bind(None, reason=None)
signature(view).bind(None, reason=None)
except TypeError:
msg = (
"The CSRF failure view '%s' does not take the correct number of "
Expand Down
5 changes: 2 additions & 3 deletions django/core/checks/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import inspect
from collections import Counter

from django.conf import settings
from django.core.exceptions import ViewDoesNotExist
from django.utils.inspect import signature

from . import Error, Tags, Warning, register

Expand Down Expand Up @@ -142,10 +142,9 @@ def check_custom_error_handlers(app_configs, **kwargs):
).format(status_code=status_code, path=path)
errors.append(Error(msg, hint=str(e), id="urls.E008"))
continue
signature = inspect.signature(handler)
args = [None] * num_parameters
try:
signature.bind(*args)
signature(handler).bind(*args)
except TypeError:
msg = (
"The custom handler{status_code} view '{path}' does not "
Expand Down
18 changes: 18 additions & 0 deletions django/db/models/aggregates.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,24 @@ def as_mysql(self, compiler, connection, **extra_context):
return sql, (*params, *delimiter_params)

def as_sqlite(self, compiler, connection, **extra_context):
if (
self.distinct
and isinstance(self.delimiter.value, Value)
and self.delimiter.value.value == ","
):
clone = self.copy()
source_expressions = clone.get_source_expressions()
clone.set_source_expressions(
source_expressions[:1] + source_expressions[2:]
)

return clone.as_sql(
compiler,
connection,
function="GROUP_CONCAT",
**extra_context,
)

if connection.get_database_version() < (3, 44):
return self.as_sql(
compiler,
Expand Down
4 changes: 2 additions & 2 deletions django/db/models/expressions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import copy
import datetime
import functools
import inspect
from collections import defaultdict
from decimal import Decimal
from enum import Enum
Expand All @@ -17,6 +16,7 @@
from django.utils.deconstruct import deconstructible
from django.utils.functional import cached_property, classproperty
from django.utils.hashable import make_hashable
from django.utils.inspect import signature


class SQLiteNumericMixin:
Expand Down Expand Up @@ -523,7 +523,7 @@ class Expression(BaseExpression, Combinable):
@classproperty
@functools.lru_cache(maxsize=128)
def _constructor_signature(cls):
return inspect.signature(cls.__init__)
return signature(cls.__init__)

@classmethod
def _identity(cls, value):
Expand Down
18 changes: 17 additions & 1 deletion django/http/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class HttpRequest:
# The encoding used in GET/POST dicts. None means use default setting.
_encoding = None
_upload_handlers = []
_multipart_parser_class = MultiPartParser

def __init__(self):
# WARNING: The `WSGIRequest` subclass doesn't call `super`.
Expand Down Expand Up @@ -364,6 +365,19 @@ def upload_handlers(self, upload_handlers):
)
self._upload_handlers = upload_handlers

@property
def multipart_parser_class(self):
return self._multipart_parser_class

@multipart_parser_class.setter
def multipart_parser_class(self, multipart_parser_class):
if hasattr(self, "_files"):
raise RuntimeError(
"You cannot set the multipart parser class after the upload has been "
"processed."
)
self._multipart_parser_class = multipart_parser_class

def parse_file_upload(self, META, post_data):
"""Return a tuple of (POST QueryDict, FILES MultiValueDict)."""
self.upload_handlers = ImmutableList(
Expand All @@ -373,7 +387,9 @@ def parse_file_upload(self, META, post_data):
"processed."
),
)
parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
parser = self.multipart_parser_class(
META, post_data, self.upload_handlers, self.encoding
)
return parser.parse()

@property
Expand Down
6 changes: 3 additions & 3 deletions django/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
from django.utils.deprecation import django_file_prefixes
from django.utils.formats import localize
from django.utils.html import conditional_escape
from django.utils.inspect import lazy_annotations
from django.utils.inspect import lazy_annotations, signature
from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import SafeData, SafeString, mark_safe
from django.utils.text import get_text_list, smart_split, unescape_string_literal
Expand Down Expand Up @@ -1000,12 +1000,12 @@ def _resolve_lookup(self, context):
current = current()
except TypeError:
try:
signature = inspect.signature(current)
current_signature = signature(current)
except ValueError: # No signature found.
current = context.template.engine.string_if_invalid
else:
try:
signature.bind()
current_signature.bind()
except TypeError: # Arguments *were* required.
# Invalid method call.
current = context.template.engine.string_if_invalid
Expand Down
3 changes: 2 additions & 1 deletion django/utils/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from asgiref.sync import sync_to_async

import django
from django.utils.inspect import signature


@functools.cache
Expand Down Expand Up @@ -163,7 +164,7 @@ def decorator(func):
if isinstance(func, staticmethod):
raise TypeError("Apply @staticmethod before @deprecate_posargs.")

params = inspect.signature(func).parameters
params = signature(func).parameters
num_by_kind = Counter(param.kind for param in params.values())

if num_by_kind[inspect.Parameter.VAR_POSITIONAL] > 0:
Expand Down
22 changes: 12 additions & 10 deletions django/utils/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,7 @@

@functools.lru_cache(maxsize=512)
def _get_func_parameters(func, remove_first):
# As the annotations are not used in any case, inspect the signature with
# FORWARDREF to leave any deferred annotations unevaluated.
if PY314:
signature = inspect.signature(
func, annotation_format=annotationlib.Format.FORWARDREF
)
else:
signature = inspect.signature(func)

parameters = tuple(signature.parameters.values())
parameters = tuple(signature(func).parameters.values())
if remove_first:
parameters = parameters[1:]
return parameters
Expand Down Expand Up @@ -130,3 +121,14 @@ def lazy_annotations():
yield
finally:
inspect._signature_from_callable = original_helper


def signature(obj):
"""
A wrapper around inspect.signature that leaves deferred annotations
unevaluated on Python 3.14+, since they are not used in our case.
"""
if PY314:
return inspect.signature(obj, annotation_format=annotationlib.Format.FORWARDREF)
else:
return inspect.signature(obj)
4 changes: 2 additions & 2 deletions docs/internals/contributing/new-contributors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ Sign the Contributor License Agreement
--------------------------------------

The code that you write belongs to you or your employer. If your contribution
is more than one or two lines of code, you need to sign the `CLA`_. See the
`Contributor License Agreement FAQ`_ for a more thorough explanation.
is more than one or two lines of code, you have the option to sign the `CLA`_.
See the `Contributor License Agreement FAQ`_ for a more thorough explanation.

.. _CLA: https://www.djangoproject.com/foundation/cla/
.. _Contributor License Agreement FAQ: https://www.djangoproject.com/foundation/cla/faq/
Expand Down
12 changes: 6 additions & 6 deletions docs/internals/contributing/writing-code/submitting-patches.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,10 @@ and time availability), claim it by following these steps:
* Finally click the "Submit changes" button at the bottom to save.

.. note::
The Django software foundation requests that anyone contributing more than
a :ref:`trivial change <trivial-change>`, to Django sign and submit a
`Contributor License Agreement`_, this ensures that the Django Software
Foundation has clear license to all contributions allowing for a clear
license for all users.
If your change is not :ref:`trivial <trivial-change>`, you have the option
to sign and submit a `Contributor License Agreement`_ clarifying the status
of your contribution. This ensures that the Django Software Foundation has
clear license to your contribution.

.. _Login using your GitHub account: https://code.djangoproject.com/github/login
.. _Create an account: https://www.djangoproject.com/accounts/register/
Expand Down Expand Up @@ -508,7 +507,8 @@ All tickets
* Is the pull request a single squashed commit with a message that follows our
:ref:`commit message format <committing-guidelines>`?
* Are you the patch author and a new contributor? Please add yourself to the
:source:`AUTHORS` file and submit a `Contributor License Agreement`_.
:source:`AUTHORS` file. At your option, submit a
`Contributor License Agreement`_.
* Does this have an accepted ticket on Trac? All contributions require a ticket
unless the :ref:`change is considered trivial <trivial-change>`.

Expand Down
13 changes: 12 additions & 1 deletion docs/ref/contrib/postgres/aggregates.txt
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,18 @@ General-purpose aggregation functions

.. attribute:: delimiter

Required argument. Needs to be a string.
Required argument. A string, :class:`~django.db.models.Value`, or
expression representing the string for separating values. For example,
``Value(",")``.

.. versionadded:: 6.0

Support for providing a ``Value`` or expression rather than a
string was added.

.. deprecated:: 6.0

Support for providing a string is deprecated.

.. attribute:: distinct

Expand Down
9 changes: 8 additions & 1 deletion docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4175,7 +4175,14 @@ by the aggregate.
.. attribute:: delimiter

A ``Value`` or expression representing the string that should separate
each of the values. For example, ``Value(",")``.
each of the values. For example, ``Value(",")``. (On SQLite, the
literal delimiter ``Value(",")`` is the only delimiter compatible with
``distinct=True``.)

.. versionchanged:: 6.1

Support for using ``distinct=True`` with a delimiter of
``Value(",")`` on SQLite was added.

Query-related tools
===================
Expand Down
24 changes: 24 additions & 0 deletions docs/ref/request-response.txt
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,30 @@ All attributes should be considered read-only, unless stated otherwise.
executed before URL resolving takes place (you can use it in
:meth:`process_view` though).

.. attribute:: HttpRequest.multipart_parser_class

.. versionadded:: 6.1

The class used to parse ``multipart/form-data`` request data. By default,
this is ``django.http.multipartparser.MultiPartParser``.

You can set this attribute to use a custom multipart parser, either via
middleware or directly in views::

from django.http.multipartparser import MultiPartParser


class CustomMultiPartParser(MultiPartParser):
def parse(self):
post = QueryDict(mutable=True)
files = MultiValueDict()
# Custom processing logic here
return post, files


# In middleware or view:
request.multipart_parser_class = CustomMultiPartParser

Attributes set by application code
----------------------------------

Expand Down
13 changes: 13 additions & 0 deletions docs/releases/5.2.12.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
===========================
Django 5.2.12 release notes
===========================

*Expected March 3, 2026*

Django 5.2.12 fixes one bug related to support for Python 3.14.

Bugfixes
========

* Fixed :exc:`NameError` when inspecting functions making use of deferred
annotations in Python 3.14 (:ticket:`36903`).
3 changes: 2 additions & 1 deletion docs/releases/6.0.3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ Django 6.0.3 fixes several bugs in 6.0.2.
Bugfixes
========

* ...
* Fixed :exc:`NameError` when inspecting functions making use of deferred
annotations in Python 3.14 (:ticket:`36903`).
Loading