From 703283b5b455a8410b4ce0a2e6ea0e1bde7f3047 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 20 Sep 2026 13:50:05 -0400 Subject: [PATCH 1/3] Fixed #37358 -- Dropped support for GDAL 3.3 and 3.4. --- django/contrib/gis/gdal/libgdal.py | 4 ---- django/contrib/gis/gdal/raster/const.py | 4 ++-- docs/ref/contrib/gis/gdal.txt | 4 ++-- docs/ref/contrib/gis/install/geolibs.txt | 10 ++++------ docs/releases/6.2.txt | 6 ++++-- tests/gis_tests/inspectapp/tests.py | 23 +++++++---------------- 6 files changed, 19 insertions(+), 32 deletions(-) diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 38554458daee..4ab0406e11a4 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -31,8 +31,6 @@ "gdal307", "gdal306", "gdal305", - "gdal304", - "gdal303", ] elif os.name == "posix": # *NIX library names. @@ -48,8 +46,6 @@ "gdal3.7.0", "gdal3.6.0", "gdal3.5.0", - "gdal3.4.0", - "gdal3.3.0", ] else: raise ImproperlyConfigured('GDAL is unsupported on OS "%s".' % os.name) diff --git a/django/contrib/gis/gdal/raster/const.py b/django/contrib/gis/gdal/raster/const.py index a1ab62f37aa2..b158478d2865 100644 --- a/django/contrib/gis/gdal/raster/const.py +++ b/django/contrib/gis/gdal/raster/const.py @@ -29,8 +29,8 @@ 9: "GDT_CInt32", # Complex Int32 10: "GDT_CFloat32", # Complex Float32 11: "GDT_CFloat64", # Complex Float64 - 12: "GDT_UInt64", # 64 bit unsigned integer (GDAL 3.5+). - 13: "GDT_Int64", # 64 bit signed integer (GDAL 3.5+). + 12: "GDT_UInt64", # 64 bit unsigned integer + 13: "GDT_Int64", # 64 bit signed integer 14: "GDT_Int8", # 8 bit signed integer (GDAL 3.7+). } diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt index ee1850be5ee1..67c67381db82 100644 --- a/docs/ref/contrib/gis/gdal.txt +++ b/docs/ref/contrib/gis/gdal.txt @@ -1946,8 +1946,8 @@ Key Default Usage 5 GDT_Int32 32 bit signed integer 6 GDT_Float32 32 bit floating point 7 GDT_Float64 64 bit floating point - 12 GDT_UInt64 64 bit unsigned integer (GDAL 3.5+) - 13 GDT_Int64 64 bit signed integer (GDAL 3.5+) + 12 GDT_UInt64 64 bit unsigned integer + 13 GDT_Int64 64 bit signed integer 14 GDT_Int8 8 bit signed integer (GDAL 3.7+) ===== =============== =================================== diff --git a/docs/ref/contrib/gis/install/geolibs.txt b/docs/ref/contrib/gis/install/geolibs.txt index f4733ae1762c..99a3125cbd0d 100644 --- a/docs/ref/contrib/gis/install/geolibs.txt +++ b/docs/ref/contrib/gis/install/geolibs.txt @@ -10,16 +10,16 @@ Geospatial libraries GeoDjango uses and/or provides interfaces for the following open source geospatial libraries: -============================== ==================================== ================================ ========================================================= +============================== ==================================== ================================ =============================================== Program Description Required Supported Versions -============================== ==================================== ================================ ========================================================= +============================== ==================================== ================================ =============================================== :ref:`GEOS ` Geometry Engine Open Source Yes 3.15, 3.14, 3.13, 3.12, 3.11 `PROJ`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 9.x, 8.x, 7.x, 6.x -:ref:`GDAL ` Geospatial Data Abstraction Library Yes 3.13, 3.12, 3.11, 3.10, 3.9, 3.8, 3.7, 3.6, 3.5, 3.4, 3.3 +:ref:`GDAL ` Geospatial Data Abstraction Library Yes 3.13, 3.12, 3.11, 3.10, 3.9, 3.8, 3.7, 3.6, 3.5 :ref:`GeoIP ` IP-based geolocation library No 2 `PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 3.6, 3.5, 3.4, 3.3 `SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 5.1, 5.0, 4.3 -============================== ==================================== ================================ ========================================================= +============================== ==================================== ================================ =============================================== Note that older or more recent versions of these libraries *may* also work totally fine with GeoDjango. Your mileage may vary. @@ -31,8 +31,6 @@ totally fine with GeoDjango. Your mileage may vary. GEOS 3.13.0 2024-09-06 GEOS 3.14.0 2025-08-21 GEOS 3.15.0 2026-09-01 - GDAL 3.3.0 2021-05-03 - GDAL 3.4.0 2021-11-04 GDAL 3.5.0 2022-05-13 GDAL 3.6.0 2022-11-03 GDAL 3.7.0 2023-05-10 diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index 2995b049f5c2..776caf684cac 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -300,14 +300,16 @@ backends. :mod:`django.contrib.gis` ------------------------- -* Support for GEOS 3.10 is removed. - * The seconds value returned by :meth:`~django.contrib.gis.gdal.Field.as_datetime` is now a ``c_float`` rather than a ``c_int``. * Support for PostGIS 3.2 is removed. +* Support for GDAL 3.3 and 3.4 is removed. + +* Support for GEOS 3.10 is removed. + Models ------ diff --git a/tests/gis_tests/inspectapp/tests.py b/tests/gis_tests/inspectapp/tests.py index fd6fec32d991..e4373331618b 100644 --- a/tests/gis_tests/inspectapp/tests.py +++ b/tests/gis_tests/inspectapp/tests.py @@ -2,7 +2,7 @@ import re from io import StringIO -from django.contrib.gis.gdal import GDAL_VERSION, Driver, GDALException +from django.contrib.gis.gdal import Driver, GDALException from django.contrib.gis.utils.ogrinspect import ogrinspect from django.core.management import call_command from django.db import connection, connections @@ -142,26 +142,17 @@ def test_time_field(self): ) ) - # The ordering of model fields might vary depending on several factors - # (version of GDAL, etc.). - if connection.vendor == "sqlite" and GDAL_VERSION < (3, 4): - # SpatiaLite introspection is somewhat lacking on GDAL < 3.4 - # (#29461). - self.assertIn(" f_decimal = models.CharField(max_length=0)", model_def) - else: - self.assertIn( - " f_decimal = models.DecimalField(max_digits=0, decimal_places=0)", - model_def, - ) + # The ordering of model fields might vary depending on several factors. + self.assertIn( + " f_decimal = models.DecimalField(max_digits=0, decimal_places=0)", + model_def, + ) self.assertIn(" f_int = models.IntegerField()", model_def) if not connection.ops.mariadb: # Probably a bug between GDAL and MariaDB on time fields. self.assertIn(" f_datetime = models.DateTimeField()", model_def) self.assertIn(" f_time = models.TimeField()", model_def) - if connection.vendor == "sqlite" and GDAL_VERSION < (3, 4): - self.assertIn(" f_float = models.CharField(max_length=0)", model_def) - else: - self.assertIn(" f_float = models.FloatField()", model_def) + self.assertIn(" f_float = models.FloatField()", model_def) max_length = 0 if connection.vendor == "sqlite" else 10 self.assertIn( " f_char = models.CharField(max_length=%s)" % max_length, model_def From 9d977aaa2d037cf0a6e2fcad40763305fe88cfda Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Sun, 19 Apr 2026 12:37:39 +0300 Subject: [PATCH 2/3] Fixed #37051 -- Improved organisation of documentation home page. The various domains of concern expressed on the home page (not always explicitly) have been re-ordered and the topics within them reorganised where appropriate. The new order is: * entry-point (installation, tutorial) * conceptual and functional layers (models, views, templates, other core functionality) * features (divided in to user-facing and developer-facing features) * resources and interfaces (database, storage, etc) * quality demands (security, performance, etc) * developing with Django * contributing to Django Two sections ("How the documentation is organized" and "Getting help") have been moved lower down the page; they don't need to appear above the main map of contents). Thanks James Bligh, James Beard, and Carlton Gibson for reviews. --- docs/index.txt | 420 +++++++++++++++++++++++-------------------------- 1 file changed, 201 insertions(+), 219 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 9ff54c389f57..0456dcd50e9a 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -9,14 +9,15 @@ Django documentation First steps =========== -Are you new to Django or to programming? This is the place to start! - -* **From scratch:** +* **Getting started**: :doc:`Overview ` | :doc:`Installation ` -* **Tutorial:** - :doc:`Part 1: Requests and responses ` | +The tutorial takes you by the hand through a series of steps to create a +web application. + +* **Tutorial**: + :doc:`Part 1: Setting things up ` | :doc:`Part 2: Models and the admin site ` | :doc:`Part 3: Views and templates ` | :doc:`Part 4: Forms and generic views ` | @@ -25,123 +26,76 @@ Are you new to Django or to programming? This is the place to start! :doc:`Part 7: Customizing the admin site ` | :doc:`Part 8: Adding third-party packages ` -* **Advanced Tutorials:** +After that, there are some tutorials dedicated to particular topics. + +* **Follow-on tutorials**: :doc:`How to write reusable apps ` | :doc:`Writing your first contribution to Django ` -Getting help -============ - -Having trouble? We'd like to help! - -* Try the :doc:`FAQ ` -- it's got answers to many common questions. - -* Looking for specific information? Try the :ref:`genindex`, :ref:`modindex` or - the :doc:`detailed table of contents `. - -* Not found anything? See :doc:`/faq/help` for information on getting support - and asking questions to the community. - -* Report bugs with Django in our `ticket tracker`_. - -.. _ticket tracker: https://code.djangoproject.com/ - -How the documentation is organized -================================== - -Django has a lot of documentation. A high-level overview of how it's organized -will help you know where to look for certain things: - -* :doc:`Tutorials ` take you by the hand through a series of - steps to create a web application. Start here if you're new to Django or web - application development. Also look at the ":ref:`index-first-steps`". - -* :doc:`Topic guides ` discuss key topics and concepts at a - fairly high level and provide useful background information and explanation. - -* :doc:`Reference guides ` contain technical reference for APIs and - other aspects of Django's machinery. They describe how it works and how to - use it but assume that you have a basic understanding of key concepts. - -* :doc:`How-to guides ` are recipes. They guide you through the - steps involved in addressing key problems and use-cases. They are more - advanced than tutorials and assume some knowledge of how Django works. - The model layer =============== -Django provides an abstraction layer (the "models") for structuring and -manipulating the data of your web application. Learn more about it below: +A Django *model* is a representation of the objects your application will +handle, described in Python, and stored in a database. -* **Models:** +* **Models**: :doc:`Introduction to models ` | :doc:`Field types ` | + :doc:`Custom fields ` | :doc:`Indexes ` | :doc:`Meta options ` | - :doc:`Model class ` + :doc:`Model class ` | + :doc:`Managers ` + +* **Model instances and relations**: + :doc:`Instance methods ` | + :doc:`Accessing related objects ` | + :doc:`Content types and generic relations ` -* **QuerySets:** +* **Queries**: :doc:`Making queries ` | :doc:`QuerySet method reference ` | - :doc:`Lookup expressions ` - -* **Model instances:** - :doc:`Instance methods ` | - :doc:`Accessing related objects ` + :doc:`Lookup expressions ` | + :doc:`Query Expressions ` | + :doc:`Conditional Expressions ` | + :doc:`Custom lookups ` | + :doc:`Aggregation ` | + :doc:`Database Functions ` | + :doc:`Raw SQL ` -* **Migrations:** +* **Migrations**: :doc:`Introduction to Migrations` | :doc:`Operations reference ` | :doc:`SchemaEditor ` | :doc:`Writing migrations ` -* **Advanced:** - :doc:`Managers ` | - :doc:`Raw SQL ` | +* **Other**: :doc:`Transactions ` | - :doc:`Aggregation ` | :doc:`Search ` | - :doc:`Custom fields ` | :doc:`Multiple databases ` | - :doc:`Custom lookups ` | - :doc:`Query Expressions ` | - :doc:`Conditional Expressions ` | - :doc:`Database Functions ` - -* **Other:** - :doc:`Supported databases ` | - :doc:`Legacy databases ` | :doc:`Providing initial data ` | - :doc:`Optimize database access ` | :doc:`PostgreSQL specific features ` The view layer ============== -Django has the concept of "views" to encapsulate the logic responsible for -processing a user's request and for returning the response. Find all you need -to know about views via the links below: +Django *views* contain the logic responsible for +processing a user's request and for returning the response. -* **The basics:** +* **The basics**: :doc:`URLconfs ` | :doc:`View functions ` | :doc:`Shortcuts ` | :doc:`Decorators ` | - :doc:`Asynchronous Support ` + :doc:`Asynchronous Support ` | + :doc:`Conditional view processing ` -* **Reference:** +* **Reference**: :doc:`Built-in Views ` | :doc:`Request/response objects ` | :doc:`TemplateResponse objects ` -* **File uploads:** - :doc:`Overview ` | - :doc:`File objects ` | - :doc:`Storage API ` | - :doc:`Managing files ` | - :doc:`Custom storage ` - -* **Class-based views:** +* **Class-based views**: :doc:`Overview ` | :doc:`Built-in display views ` | :doc:`Built-in editing views ` | @@ -149,201 +103,229 @@ to know about views via the links below: :doc:`API reference ` | :doc:`Flattened index ` -* **Advanced:** - :doc:`Generating CSV ` | - :doc:`Generating PDF ` - -* **Middleware:** +* **Middleware**: :doc:`Overview ` | :doc:`Built-in middleware classes ` +* **Other output formats**: + :doc:`Generating CSV ` | + :doc:`Generating PDF ` + The template layer ================== The template layer provides a designer-friendly syntax for rendering the -information to be presented to the user. Learn how this syntax can be used by -designers and how it can be extended by programmers: +information to be presented to the user. Template functionality can also be +customized. -* **The basics:** - :doc:`Overview ` - -* **For designers:** +* **The basics**: + :doc:`Overview ` | :doc:`Language overview ` | :doc:`Built-in tags and filters ` | - :doc:`Humanization ` + :doc:`Humanization ` | + :doc:`Template API ` -* **For programmers:** - :doc:`Template API ` | +* **Customization and extension**: :doc:`Custom tags and filters ` | :doc:`Custom template backend ` -Forms -===== - -Django provides a rich framework to facilitate the creation of forms and the -manipulation of form data. - -* **The basics:** - :doc:`Overview ` | - :doc:`Form API ` | - :doc:`Built-in fields ` | - :doc:`Built-in widgets ` +Other core functionality +======================== -* **Advanced:** - :doc:`Forms for models ` | - :doc:`Integrating media ` | - :doc:`Formsets ` | - :doc:`Customizing validation ` - -The development process -======================= - -Learn about the various components and tools to help you in the development and -testing of Django applications: - -* **Settings:** +* **Settings**: :doc:`Overview ` | :doc:`Full list of settings ` -* **Applications:** +* **Applications**: :doc:`Overview ` -* **Exceptions:** +* **Exceptions**: :doc:`Overview ` -* **django-admin and manage.py:** +* **Management commands**: :doc:`Overview ` | :doc:`Adding custom commands ` -* **Testing:** - :doc:`Introduction ` | - :doc:`Writing and running tests ` | - :doc:`Included testing tools ` | - :doc:`Advanced topics ` +User-facing features +==================== -* **Deployment:** - :doc:`Overview ` | - :doc:`WSGI servers ` | - :doc:`ASGI servers ` | - :doc:`Deploying static files ` | - :doc:`Tracking code errors by email ` | - :doc:`Deployment checklist ` +* **Admin**: + :doc:`Admin site ` | + :doc:`Admin actions ` | + :doc:`Admin documentation generator ` -The admin -========= +* **Content**: + :doc:`Flatpages ` | + :doc:`Redirects ` | + :doc:`Unicode in Django ` | + :doc:`Pagination ` | + :doc:`Messages framework ` -Find all you need to know about the automated admin interface, one of Django's -most popular features: +* **Forms**: + :doc:`Overview ` | + :doc:`Form API ` | + :doc:`Built-in fields ` | + :doc:`Built-in widgets ` | + :doc:`Forms for models ` | + :doc:`Integrating media ` | + :doc:`Formsets ` | + :doc:`Customizing validation ` -* :doc:`Admin site ` -* :doc:`Admin actions ` -* :doc:`Admin documentation generator ` +* **Internationalization and localization**: + :doc:`Overview ` | + :doc:`Internationalization ` | + :ref:`Localization ` | + :doc:`Localized web UI formatting and form input ` | + :doc:`Time zones ` -Security -======== +* **Authentication**: + :doc:`Overview ` | + :doc:`Using the authentication system ` | + :doc:`Password management ` | + :doc:`Customizing authentication ` | + :doc:`API Reference ` -Security is a topic of paramount importance in the development of web -applications and Django provides multiple protection tools and mechanisms: +* **Geographic data and representation**: + :doc:`GeoDjango ` -* :doc:`Security overview ` -* :doc:`Disclosed security issues in Django ` -* :doc:`Clickjacking protection ` -* :doc:`Cross Site Request Forgery protection ` -* :doc:`Cryptographic signing ` -* :ref:`Security Middleware ` -* :doc:`Content Security Policy ` +Developer-facing features +========================= -Internationalization and localization -===================================== +* **Core features**: + :doc:`The sites framework ` | + :doc:`Sessions ` | + :doc:`Static files management ` -Django offers a robust internationalization and localization framework to -assist you in the development of applications for multiple languages and world -regions: +* **Project-level features**: + :doc:`Caching ` | + :doc:`Logging ` | + :doc:`System check framework ` -* :doc:`Overview ` | - :doc:`Internationalization ` | - :ref:`Localization ` | - :doc:`Localized web UI formatting and form input ` -* :doc:`Time zones ` +* **Backend processing**: + :doc:`Tasks framework ` | + :doc:`Signals ` -Performance and optimization -============================ +* **Data management**: + :doc:`Serialization ` | + :doc:`Data validation ` -There are a variety of techniques and tools that can help get your code running -more efficiently - faster, and using fewer system resources. +* **Discoverability**: + :doc:`Sitemaps ` | + :doc:`Syndication feeds (RSS/Atom) ` -* :doc:`Performance and optimization overview ` +Resources and interfaces +======================== -Geographic framework -==================== +* **Databases**: + :doc:`Supported databases ` | + :doc:`Legacy databases ` -:doc:`GeoDjango ` intends to be a world-class -geographic web framework. Its goal is to make it as easy as possible to build -GIS web applications and harness the power of spatially enabled data. +* **Storage**: + :doc:`Overview ` | + :doc:`File objects ` | + :doc:`Storage API ` | + :doc:`Managing files ` | + :doc:`Custom storage ` -Common web application tools -============================ +* **Deployment**: + :doc:`Overview ` | + :doc:`WSGI servers ` | + :doc:`ASGI servers ` | + :doc:`Deploying static files ` | + :doc:`Tracking code errors by email ` | + :doc:`Deployment checklist ` -Django offers multiple tools commonly needed in the development of web -applications: +* **Email**: + :doc:`Sending messages ` -* **Authentication:** - :doc:`Overview ` | - :doc:`Using the authentication system ` | - :doc:`Password management ` | - :doc:`Customizing authentication ` | - :doc:`API Reference ` -* :doc:`Caching ` -* :doc:`Logging ` -* :doc:`Tasks framework ` -* :doc:`Sending emails ` -* :doc:`Syndication feeds (RSS/Atom) ` -* :doc:`Pagination ` -* :doc:`Messages framework ` -* :doc:`Serialization ` -* :doc:`Sessions ` -* :doc:`Sitemaps ` -* :doc:`Static files management ` -* :doc:`Data validation ` - -Other core functionalities -========================== - -Learn about some other core functionalities of the Django framework: - -* :doc:`Conditional content processing ` -* :doc:`Content types and generic relations ` -* :doc:`Flatpages ` -* :doc:`Redirects ` -* :doc:`Signals ` -* :doc:`System check framework ` -* :doc:`The sites framework ` -* :doc:`Unicode in Django ` +Quality and dependability +========================= + +A key concern in Django's design philosophy is to make it easier to build +applications that work safely and efficiently, in the real world. + +* **Testing**: + :doc:`Introduction ` | + :doc:`Writing and running tests ` | + :doc:`Included testing tools ` | + :doc:`Advanced topics ` + +* **Security**: + :doc:`Security overview ` | + :doc:`Disclosed security issues in Django ` | + :doc:`Clickjacking protection ` | + :doc:`Cross Site Request Forgery protection ` | + :doc:`Cryptographic signing ` | + :ref:`Security Middleware ` | + :doc:`Content Security Policy ` + +* **Performance**: + :doc:`Performance and optimization overview ` | + :doc:`Optimize database access ` The Django open-source project ============================== -Learn about the development process for the Django project itself and about how -you can contribute: +Django moves forward steadily, thanks to a rigorous development process and a +supportive community that welcomes new contributors: -* **Community:** +* **Community**: :doc:`Contributing to Django ` | - :doc:`The release process ` | :doc:`Team organization ` | :doc:`The Django source code repository ` | - :doc:`Security policies ` | :doc:`Mailing lists and Forum ` -* **Design philosophies:** +* **Processes and policies**: + :doc:`The release process ` | + :doc:`Security policies ` + +* **Design philosophies**: :doc:`Overview ` -* **Documentation:** +* **Documentation**: :doc:`About this documentation ` -* **Third-party distributions:** +* **Third-party distributions**: :doc:`Overview ` -* **Django over time:** +* **Django over time**: :doc:`API stability ` | :doc:`Release notes and upgrading instructions ` | :doc:`Deprecation Timeline ` + +Getting help +============ + +Having trouble? We'd like to help! + +* Try the :doc:`FAQ ` -- it's got answers to many common questions. + +* Looking for specific information? Try the :ref:`genindex`, :ref:`modindex` or + the :doc:`detailed table of contents `. + +* Not found anything? See :doc:`/faq/help` for information on getting support + and asking questions to the community. + +* Report bugs with Django in our `ticket tracker`_. + +.. _ticket tracker: https://code.djangoproject.com/ + +How the documentation is organized +================================== + +The table of contents above unfolds Django's documentation thematically. Pages +themselves are organized into an underlying hierarchy according to type, to +help navigate the site: + +* The :doc:`tutorial ` takes you by the hand through a series of + steps to create a web application. Start here if you're new to Django or web + application development. + +* :doc:`Topic guides ` explain Django's functionality in detail, + and provide an overview of key topics and concepts. + +* :doc:`Reference guides ` contain technical reference for APIs and + other aspects of Django's machinery. + +* :doc:`How-to guides ` guide you through the work to address + real-world problems and use-cases. From a013c821ea7838a953869615b0bdcfdb298cc09f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 19 Jun 2026 14:40:37 -0400 Subject: [PATCH 3/3] Fixed #37177 -- Reduced per-middleware context switching under ASGI. Relying on MiddlewareMixin.__acall__() to handle async requests imposed per-middleware context switching, defeating BaseHandler's intent to transition only once per middleware chain. To minimize breaking changes, MiddlewareMixin's default value did not change: only the values on Django's own subclasses. Use cases where the prior behavior is preferable (in order to avoid pinning a thread per request) are fleshed out in docs. Thanks Mykhailo Havelia for the report. --- django/contrib/auth/middleware.py | 2 + django/contrib/flatpages/middleware.py | 2 + django/contrib/messages/middleware.py | 2 + django/contrib/redirects/middleware.py | 2 + django/contrib/sessions/middleware.py | 2 + django/contrib/sites/middleware.py | 2 + django/middleware/cache.py | 4 + django/middleware/clickjacking.py | 2 + django/middleware/common.py | 2 + django/middleware/csp.py | 2 + django/middleware/csrf.py | 2 + django/middleware/gzip.py | 1 + django/middleware/http.py | 2 + django/middleware/locale.py | 1 + django/middleware/security.py | 2 + docs/releases/6.2.txt | 16 ++++ docs/topics/http/middleware.txt | 20 ++++- tests/middleware_exceptions/tests.py | 102 ++++++++++++++++++++++++- tests/middleware_exceptions/urls.py | 1 + tests/middleware_exceptions/views.py | 4 + 20 files changed, 169 insertions(+), 4 deletions(-) diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py index f3735c501daa..7bda01e5117f 100644 --- a/django/contrib/auth/middleware.py +++ b/django/contrib/auth/middleware.py @@ -27,6 +27,8 @@ async def auser(request): class AuthenticationMiddleware(MiddlewareMixin): + async_capable = False + def process_request(self, request): if not hasattr(request, "session"): raise ImproperlyConfigured( diff --git a/django/contrib/flatpages/middleware.py b/django/contrib/flatpages/middleware.py index 00017ad4daea..3fa82a559a8a 100644 --- a/django/contrib/flatpages/middleware.py +++ b/django/contrib/flatpages/middleware.py @@ -5,6 +5,8 @@ class FlatpageFallbackMiddleware(MiddlewareMixin): + async_capable = False + def process_response(self, request, response): if response.status_code != 404: return response # No need to check for a flatpage for non-404 responses. diff --git a/django/contrib/messages/middleware.py b/django/contrib/messages/middleware.py index c3bf59d0b2ed..7b58930868d7 100644 --- a/django/contrib/messages/middleware.py +++ b/django/contrib/messages/middleware.py @@ -8,6 +8,8 @@ class MessageMiddleware(MiddlewareMixin): Middleware that handles temporary messages. """ + async_capable = False + def process_request(self, request): request._messages = default_storage(request) diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index 3c4beb7e889e..97f430cf28d6 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -8,6 +8,8 @@ class RedirectFallbackMiddleware(MiddlewareMixin): + async_capable = False + # Defined as class-level attributes to be subclassing-friendly. response_gone_class = HttpResponseGone response_redirect_class = HttpResponsePermanentRedirect diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py index 06217b3dacbc..a0b27ea42961 100644 --- a/django/contrib/sessions/middleware.py +++ b/django/contrib/sessions/middleware.py @@ -10,6 +10,8 @@ class SessionMiddleware(MiddlewareMixin): + async_capable = False + def __init__(self, get_response): super().__init__(get_response) engine = import_module(settings.SESSION_ENGINE) diff --git a/django/contrib/sites/middleware.py b/django/contrib/sites/middleware.py index 6333283f3edd..883c2b382651 100644 --- a/django/contrib/sites/middleware.py +++ b/django/contrib/sites/middleware.py @@ -8,5 +8,7 @@ class CurrentSiteMiddleware(MiddlewareMixin): Middleware that sets `site` attribute to request object. """ + async_capable = False + def process_request(self, request): request.site = get_current_site(request) diff --git a/django/middleware/cache.py b/django/middleware/cache.py index ea6decb83c23..1fd7ac0a703c 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -71,6 +71,8 @@ class UpdateCacheMiddleware(MiddlewareMixin): so that it'll get called last during the response phase. """ + async_capable = False + def __init__(self, get_response): super().__init__(get_response) self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS @@ -153,6 +155,8 @@ class FetchFromCacheMiddleware(MiddlewareMixin): so that it'll get called last during the request phase. """ + async_capable = False + def __init__(self, get_response): super().__init__(get_response) self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX diff --git a/django/middleware/clickjacking.py b/django/middleware/clickjacking.py index 1c6117963baa..bba792b3e495 100644 --- a/django/middleware/clickjacking.py +++ b/django/middleware/clickjacking.py @@ -22,6 +22,8 @@ class XFrameOptionsMiddleware(MiddlewareMixin): X_FRAME_OPTIONS in your project's Django settings to 'SAMEORIGIN'. """ + async_capable = False + def process_response(self, request, response): # Don't set it if it's already in the response if response.get("X-Frame-Options") is not None: diff --git a/django/middleware/common.py b/django/middleware/common.py index de6c94c1485d..08bd9db5b00b 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -29,6 +29,7 @@ class CommonMiddleware(MiddlewareMixin): overriding the response_redirect_class attribute. """ + async_capable = False response_redirect_class = HttpResponsePermanentRedirect def process_request(self, request): @@ -118,6 +119,7 @@ def process_response(self, request, response): class BrokenLinkEmailsMiddleware(MiddlewareMixin): + async_capable = False # Set to override the mail.mailers alias used for sending the email. using = None diff --git a/django/middleware/csp.py b/django/middleware/csp.py index 58e2b778f6cb..8ecdad878e97 100644 --- a/django/middleware/csp.py +++ b/django/middleware/csp.py @@ -8,6 +8,8 @@ def get_nonce(request): class ContentSecurityPolicyMiddleware(MiddlewareMixin): + async_capable = False + def process_request(self, request): request._csp_nonce = LazyNonce() diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index d70c3efe3fc3..7738eb225ebd 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -171,6 +171,8 @@ class CsrfViewMiddleware(MiddlewareMixin): template tag. """ + async_capable = False + @cached_property def csrf_trusted_origins_hosts(self): return [ diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py index 51ae29bcdb0c..a7be21a24440 100644 --- a/django/middleware/gzip.py +++ b/django/middleware/gzip.py @@ -13,6 +13,7 @@ class GZipMiddleware(MiddlewareMixin): on the Accept-Encoding header. """ + async_capable = False max_random_bytes = 100 def process_response(self, request, response): diff --git a/django/middleware/http.py b/django/middleware/http.py index e44d59a9604a..5362ceb1cd26 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -11,6 +11,8 @@ class ConditionalGetMiddleware(MiddlewareMixin): header if needed. """ + async_capable = False + def process_response(self, request, response): # It's too late to prevent an unsafe request with a 412 response, and # for a HEAD request, the response body is always empty so computing diff --git a/django/middleware/locale.py b/django/middleware/locale.py index baff2e343416..0a54ad21544f 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -14,6 +14,7 @@ class LocaleMiddleware(MiddlewareMixin): the language the user desires (if the language is available). """ + async_capable = False response_redirect_class = HttpResponseRedirect def process_request(self, request): diff --git a/django/middleware/security.py b/django/middleware/security.py index ee182f54493f..6c69d2652a0e 100644 --- a/django/middleware/security.py +++ b/django/middleware/security.py @@ -6,6 +6,8 @@ class SecurityMiddleware(MiddlewareMixin): + async_capable = False + def __init__(self, get_response): super().__init__(get_response) self.sts_seconds = settings.SECURE_HSTS_SECONDS diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index 776caf684cac..b135e8b5188c 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -321,6 +321,22 @@ Models ``ValidationError`` instead of being silently accepted, and surrounding whitespace is stripped. +Requests and responses +---------------------- + +* Most Django-provided middleware now set ``async_capable = False`` to + avoid repetitive context switching in ``MiddlewareMixin.__acall__()`` under + ASGI. For atypical use cases, e.g. high in-process concurrency over non-ORM + I/O, where no thread is otherwise held, the repetitive context switching may + be preferable to pinning a thread per request (see :ref:`async_performance`). + Such deployments can restore the prior behavior by setting + ``async_capable = True``; see :ref:`MiddlewareMixin `. + :class:`~django.contrib.auth.middleware.RemoteUserMiddleware` is unaffected, + because it does not use ``MiddlewareMixin``. Neither is + :class:`~django.contrib.auth.middleware.LoginRequiredMiddleware` nor + ``django.contrib.admindocs.middleware.XViewMiddleware`` affected, as they did + not implement ``process_request()`` or ``process_response()``. + Tests ----- diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index f7c57106e970..920bd548d001 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -414,9 +414,23 @@ The ``__call__()`` method: #. Calls ``self.process_response(request, response)`` (if defined). #. Returns the response. -If used with ``MIDDLEWARE_CLASSES``, the ``__call__()`` method will -never be used; Django calls ``process_request()`` and ``process_response()`` -directly. +The ``__acall__()`` method adapts sync ``process_request()`` and +``process_response()`` methods to the async context, which is usually something +to avoid, as it causes repetitive context switching (via +:func:`asgiref.sync.sync_to_async`) for each middleware. For this reason, +Django's built-in middlewares that lack an ``__acall__()`` implementation set +``async_capable = False``. Views tuned for high in-process concurrency over +non-ORM I/O (see :ref:`async_performance`) and only light CPU-bound work in +sync middleware may find this repetitive context switching preferable to the +alternative -- pinning a thread per request in sync mode -- and may wish to set +``async_capable`` to ``True``. + +.. versionchanged:: 6.2 + + In earlier versions, each of Django's middlewares without their own + ``__acall__()`` inherited the default value of + ``MiddlewareMixin.async_capable`` (``True``), making them subject to the + repetitive context switching described above. In most cases, inheriting from this mixin will be sufficient to make an old-style middleware compatible with the new system with sufficient diff --git a/tests/middleware_exceptions/tests.py b/tests/middleware_exceptions/tests.py index 4f4ce5f2f86c..f7205a538be2 100644 --- a/tests/middleware_exceptions/tests.py +++ b/tests/middleware_exceptions/tests.py @@ -1,7 +1,15 @@ +from contextlib import nullcontext +from unittest import mock + +from asgiref.sync import sync_to_async + from django.conf import settings from django.core.exceptions import MiddlewareNotUsed from django.http import HttpResponse -from django.test import RequestFactory, SimpleTestCase, override_settings +from django.middleware import MiddlewareMixin +from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings +from django.test.client import AsyncClient +from django.utils.module_loading import import_string from . import middleware as mw @@ -346,6 +354,98 @@ def test_async_and_sync_middleware_sync_call(self): self.assertEqual(response.status_code, 200) +@override_settings( + DEBUG=True, + ROOT_URLCONF="middleware_exceptions.urls", +) +class MiddlewareSyncAsyncTransitionTests(TestCase): + async def test_sync_middleware_adaptation_grouping(self): + real_acall = MiddlewareMixin.__acall__ + + async def spy_acall(self, request): + return await real_acall(self, request) + + with ( + # This middleware requires a second adaptation for process_view(), + # making it a little harder to see what's happening. + self.modify_settings( + MIDDLEWARE={"remove": "django.middleware.csrf.CsrfViewMiddleware"} + ), + mock.patch.object( + MiddlewareMixin, + "__acall__", + autospec=True, + side_effect=spy_acall, + ) as acall, + ): + with mock.patch( + "django.core.handlers.base.sync_to_async", wraps=sync_to_async + ) as base_adapter: + self.async_client.handler.load_middleware(is_async=True) + # Exit the mock of sync_to_async before issuing a request. + await self.async_client.get("/middleware_exceptions/async_view/") + + # No sync-to-async adaptation via MiddlewareMixin.__acall__(). + acall.assert_not_called() + # O(1) sync-to-async adaptation via BaseHandler. + self.assertEqual(base_adapter.call_count, 1) + + async def test_sync_middleware_adaptation_grouping_isolation(self): + real_acall = MiddlewareMixin.__acall__ + + async def spy_acall(self, request): + return await real_acall(self, request) + + for middleware in [ + # From startproject template. + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + # Other subclasses of MiddlewareMixin implementing either + # process_response() or process_request(). + "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", + "django.contrib.redirects.middleware.RedirectFallbackMiddleware", + "django.contrib.sites.middleware.CurrentSiteMiddleware", + "django.middleware.cache.FetchFromCacheMiddleware", + "django.middleware.cache.UpdateCacheMiddleware", + "django.middleware.common.BrokenLinkEmailsMiddleware", + "django.middleware.csp.ContentSecurityPolicyMiddleware", + "django.middleware.gzip.GZipMiddleware", + "django.middleware.http.ConditionalGetMiddleware", + "django.middleware.locale.LocaleMiddleware", + ]: + with ( + self.subTest(middleware=middleware), + self.settings(MIDDLEWARE=[middleware]), + ( + self.modify_settings( + INSTALLED_APPS={"append": middleware.split(".middleware")[0]} + ) + if middleware.startswith("django.contrib.") + else nullcontext() + ), + # Patch out any ImproperlyConfigured from testing in isolation. + ( + mock.patch( + f"{middleware}.process_request", + side_effect=lambda request: None, + ) + if hasattr(import_string(middleware), "process_request") + else nullcontext() + ), + mock.patch.object( + MiddlewareMixin, "__acall__", autospec=True, side_effect=spy_acall + ) as acall, + ): + client = AsyncClient() + await client.get("/middleware_exceptions/async_view/") + acall.assert_not_called() + + @override_settings(ROOT_URLCONF="middleware_exceptions.urls") class AsyncMiddlewareTests(SimpleTestCase): @override_settings( diff --git a/tests/middleware_exceptions/urls.py b/tests/middleware_exceptions/urls.py index 80cbb2c21beb..6d876a89d608 100644 --- a/tests/middleware_exceptions/urls.py +++ b/tests/middleware_exceptions/urls.py @@ -9,6 +9,7 @@ path("middleware_exceptions/exception_in_render/", views.exception_in_render), path("middleware_exceptions/template_response/", views.template_response), # Async views. + path("middleware_exceptions/async_view/", views.async_normal_view), path( "middleware_exceptions/async_exception_in_render/", views.async_exception_in_render, diff --git a/tests/middleware_exceptions/views.py b/tests/middleware_exceptions/views.py index 0f1595b2d6db..857667726a02 100644 --- a/tests/middleware_exceptions/views.py +++ b/tests/middleware_exceptions/views.py @@ -8,6 +8,10 @@ def normal_view(request): return HttpResponse("OK") +async def async_normal_view(request): + return HttpResponse("OK") + + def template_response(request): template = engines["django"].from_string( "template_response OK{% for m in mw %}\n{{ m }}{% endfor %}"