From d8cedcab5241725003eef059dd76c533f24c5430 Mon Sep 17 00:00:00 2001 From: mundur Date: Wed, 8 Jul 2026 22:38:46 +0800 Subject: [PATCH 1/5] Refs #37203 -- Escaped introspection error comments in inspectdb. --- django/core/management/commands/inspectdb.py | 4 ++-- tests/inspectdb/tests.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index 8bd6c4bc0c21..b39d55089984 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -123,8 +123,8 @@ def handle_inspection(self, options): cursor, table_name ) except Exception as e: - yield "# Unable to inspect table '%s'" % table_name - yield "# The error was: %s" % e + yield "# Unable to inspect table %r" % table_name + yield "# The error was: %r" % str(e) continue model_name = self.normalize_table_name(table_name) diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index 2a7cbc6bac94..a0b0e5403959 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -515,6 +515,26 @@ def test_introspection_errors(self): # The error message depends on the backend self.assertIn("# The error was:", output) + def test_introspection_errors_escape_table_name_and_message(self): + table_name = "table_name\ncontinued" + error_message = "error\ncontinued" + out = StringIO() + with ( + mock.patch( + "django.db.connection.introspection.get_table_list", + return_value=[TableInfo(name=table_name, type="t")], + ), + mock.patch( + "django.db.connection.introspection.get_relations", + side_effect=RuntimeError(error_message), + ), + ): + call_command("inspectdb", stdout=out) + output = out.getvalue() + self.assertIn(f"# Unable to inspect table {table_name!r}", output) + self.assertIn(f"# The error was: {error_message!r}", output) + self.assertNotIn("\ncontinued", output) + def test_same_relations(self): out = StringIO() call_command("inspectdb", "inspectdb_message", stdout=out) From 2a5da9d00555beef8e5f6307cfcbfc029d45491e Mon Sep 17 00:00:00 2001 From: mundur Date: Wed, 8 Jul 2026 22:38:10 +0800 Subject: [PATCH 2/5] Fixed #37203 -- Used normalized field names for composite pks in inspectdb. --- django/core/management/commands/inspectdb.py | 16 +++++++++++----- tests/inspectdb/models.py | 6 ++++++ tests/inspectdb/tests.py | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index b39d55089984..9aedb0d2474f 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -133,12 +133,9 @@ def handle_inspection(self, options): yield "class %s(models.Model):" % model_name known_models.append(model_name) - if len(primary_key_columns) > 1: - fields = ", ".join([f"'{col}'" for col in primary_key_columns]) - yield f" pk = models.CompositePrimaryKey({fields})" - used_column_names = [] # Holds column names used in the table so far column_to_field_name = {} # Maps column names to names of model fields + field_lines = [] used_relations = set() # Holds foreign relations used in the table. for row in table_description: comment_notes = ( @@ -252,7 +249,16 @@ def handle_inspection(self, options): field_desc += ")" if comment_notes: field_desc += " # " + " ".join(comment_notes) - yield " %s" % field_desc + field_lines.append(" %s" % field_desc) + if len(primary_key_columns) > 1: + fields = ", ".join( + [ + f"{column_to_field_name[col]!r}" + for col in primary_key_columns + ] + ) + yield f" pk = models.CompositePrimaryKey({fields})" + yield from field_lines comment = None if info := table_info.get(table_name): is_view = info.type == "v" diff --git a/tests/inspectdb/models.py b/tests/inspectdb/models.py index 489704879f35..a4838b508a37 100644 --- a/tests/inspectdb/models.py +++ b/tests/inspectdb/models.py @@ -172,6 +172,12 @@ class CompositePKModel(models.Model): column_2 = models.IntegerField() +class CompositePKModel2(models.Model): + pk = models.CompositePrimaryKey("column_1", "column_2") + column_1 = models.IntegerField(db_column="column-1") + column_2 = models.IntegerField(db_column="column-2") + + class DbOnDeleteModel(models.Model): fk_do_nothing = models.ForeignKey(UniqueTogether, on_delete=models.DO_NOTHING) fk_db_cascade = models.ForeignKey(ColumnTypes, on_delete=models.DB_CASCADE) diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index a0b0e5403959..b3319dc4ebfe 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -722,6 +722,24 @@ def test_composite_primary_key(self): self.assertIn(f"column_1 = models.{field_type}()", output) self.assertIn(f"column_2 = models.{field_type}()", output) + def test_composite_primary_key_uses_normalized_column_names(self): + out = StringIO() + field_type = connection.features.introspected_field_types["IntegerField"] + call_command("inspectdb", "inspectdb_compositepkmodel2", stdout=out) + output = out.getvalue() + self.assertIn( + "pk = models.CompositePrimaryKey('column_1', 'column_2')", + output, + ) + self.assertIn( + f"column_1 = models.{field_type}(db_column='column-1')", + output, + ) + self.assertIn( + f"column_2 = models.{field_type}(db_column='column-2')", + output, + ) + def test_composite_primary_key_not_unique_together(self): out = StringIO() call_command("inspectdb", "inspectdb_compositepkmodel", stdout=out) From 6fc8150005256db2052b01812d65dff737563a1b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sat, 18 Jul 2026 14:28:25 +0200 Subject: [PATCH 3/5] Fixed #36027 -- Made error response rendering thread-sensitive. Error response handling frequently makes use of the database, and so must use the request specific connection sensitive worker thread. Previously response_for_exception was dispatched on the event loop's default executor. This potentially leads to pool exhaustion, and deadlock, when using database pooling option, or a large number of permanently held open database connections when not using pooling. Connections held open were not subject to cleanup. If closed server side, the reported "connection is closed" would be seen until application restart. Moving error response rendering to the request specific connection sensitive worker thread resolves both of these issues. The `thread_sensitive=False` was set in e17ee4468875077b90b70bb6a589ebad7493f757 to maintain the prior behaviour when asgiref switched the thread_sensitive default to `True`. There was no specific reason for the non-tread-sensitive behaviour beyond that. (With hindsight it was always incorrect for the reasons here.) --- django/contrib/staticfiles/handlers.py | 2 +- django/core/handlers/exception.py | 2 +- docs/releases/6.2.txt | 6 +++++ tests/asgi/tests.py | 26 ++++++++++++++++++- tests/asgi/urls.py | 16 ++++++++++++ tests/staticfiles_tests/test_handlers.py | 33 ++++++++++++++++++++++-- 6 files changed, 80 insertions(+), 5 deletions(-) diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py index 686718a35501..44c1187c8e8e 100644 --- a/django/contrib/staticfiles/handlers.py +++ b/django/contrib/staticfiles/handlers.py @@ -59,7 +59,7 @@ async def get_response_async(self, request): try: return await sync_to_async(self.serve, thread_sensitive=False)(request) except Http404 as e: - return await sync_to_async(response_for_exception, thread_sensitive=False)( + return await sync_to_async(response_for_exception, thread_sensitive=True)( request, e ) diff --git a/django/core/handlers/exception.py b/django/core/handlers/exception.py index 4adc9d7c6af2..559291abfc9b 100644 --- a/django/core/handlers/exception.py +++ b/django/core/handlers/exception.py @@ -43,7 +43,7 @@ async def inner(request): response = await get_response(request) except Exception as exc: response = await sync_to_async( - response_for_exception, thread_sensitive=False + response_for_exception, thread_sensitive=True )(request, exc) return response diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index d40237d86f32..7fdbad6f665a 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -287,6 +287,12 @@ Miscellaneous * The minimum supported version of ``asgiref`` is increased from 3.9.1 to 3.12.1. +* In the asynchronous request path, error responses (such as those rendered by + ``handler404`` and ``handler500``) are now rendered on the request's + thread-sensitive thread, rather than on a shared thread pool, so that + database connections used during error handling are managed by + ``close_old_connections()``. + .. _deprecated-features-6.2: Features deprecated in 6.2 diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py index 727ce2fa6c91..a303c8146c19 100644 --- a/tests/asgi/tests.py +++ b/tests/asgi/tests.py @@ -30,7 +30,7 @@ from django.utils.http import http_date from django.views.decorators.csrf import csrf_exempt -from .urls import sync_waiter, test_filename +from .urls import permission_denied_threads, sync_waiter, test_filename TEST_STATIC_ROOT = Path(__file__).parent / "project" / "static" TOO_MUCH_DATA_MSG = "Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE." @@ -497,6 +497,30 @@ async def test_request_lifecycle_signals_dispatched_with_thread_sensitive(self): request_started_call["thread"], request_finished_call["thread"] ) + async def test_error_handler_dispatched_with_thread_sensitive(self): + """ + Error responses must be rendered on the request's thread-sensitive + thread, where the request's database connections are usable and remain + subject to close_old_connections(). + """ + permission_denied_threads.clear() + application = get_asgi_application() + scope = self.async_request_factory._base_scope(path="/permission_denied/") + communicator = ApplicationCommunicator(application, scope) + await communicator.send_input({"type": "http.request"}) + response_start = await communicator.receive_output() + self.assertEqual(response_start["type"], "http.response.start") + self.assertEqual(response_start["status"], 403) + response_body = await communicator.receive_output() + self.assertEqual(response_body["body"], b"Error handler content") + # Give response.close() time to finish. + await communicator.wait() + + self.assertEqual( + permission_denied_threads["error_handler"], + permission_denied_threads["view"], + ) + async def test_concurrent_async_uses_multiple_thread_pools(self): sync_waiter.active_threads.clear() diff --git a/tests/asgi/urls.py b/tests/asgi/urls.py index 0311cf3f76b2..a22d97b43196 100644 --- a/tests/asgi/urls.py +++ b/tests/asgi/urls.py @@ -2,6 +2,7 @@ import threading import time +from django.core.exceptions import PermissionDenied from django.http import FileResponse, HttpResponse, StreamingHttpResponse from django.urls import path from django.views.decorators.csrf import csrf_exempt @@ -51,6 +52,20 @@ def post_echo(request): sync_waiter.barrier = threading.Barrier(2) +def permission_denied_view(request): + permission_denied_threads["view"] = threading.current_thread() + raise PermissionDenied + + +def permission_denied_error_handler(request, exception=None): + permission_denied_threads["error_handler"] = threading.current_thread() + return HttpResponse("Error handler content", status=403) + + +permission_denied_threads = {} +handler403 = permission_denied_error_handler + + async def streaming_inner(sleep_time): yield b"first\n" await asyncio.sleep(sleep_time) @@ -73,5 +88,6 @@ async def streaming_view(request): path("post/", post_echo), path("wait/", sync_waiter), path("delayed_hello/", hello_with_delay), + path("permission_denied/", permission_denied_view), path("streaming/", streaming_view), ] diff --git a/tests/staticfiles_tests/test_handlers.py b/tests/staticfiles_tests/test_handlers.py index 5145d187e1a9..51509bfa706c 100644 --- a/tests/staticfiles_tests/test_handlers.py +++ b/tests/staticfiles_tests/test_handlers.py @@ -1,6 +1,11 @@ +import threading + +from asgiref.sync import sync_to_async + from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler from django.core.handlers.asgi import ASGIHandler -from django.test import AsyncRequestFactory +from django.http import HttpResponse +from django.test import AsyncRequestFactory, override_settings from .cases import StaticFilesTestCase @@ -12,6 +17,21 @@ async def __call__(self, scope, receive, send): return "Application called" +error_handler_threads = {} + + +def log_thread(key): + error_handler_threads[key] = threading.current_thread() + + +urlpatterns = [] + + +def handler404(request, exception=None): + log_thread("error_handler") + return HttpResponse("Error handler content", status=404) + + class TestASGIStaticFilesHandler(StaticFilesTestCase): async_request_factory = AsyncRequestFactory() @@ -23,11 +43,20 @@ async def test_get_async_response(self): self.assertEqual(response.status_code, 200) async def test_get_async_response_not_found(self): + error_handler_threads.clear() + await sync_to_async(log_thread)("worker") + request = self.async_request_factory.get("/static/test/not-found.txt") handler = ASGIStaticFilesHandler(ASGIHandler()) - response = await handler.get_response_async(request) + with override_settings(ROOT_URLCONF="staticfiles_tests.test_handlers"): + response = await handler.get_response_async(request) self.assertEqual(response.status_code, 404) + self.assertEqual( + error_handler_threads["error_handler"], + error_handler_threads["worker"], + ) + async def test_non_http_requests_passed_to_the_wrapped_application(self): tests = [ "/static/path.txt", From c8ad2fbb2a81d292d88a47811cebfd7d6d6e815e Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 22 Jul 2026 16:20:19 -0400 Subject: [PATCH 4/5] Updated source translation catalogs. Forwardport of ced2d118d93e33679850c061ea96bf68c70892a7 from stable/6.1.x. --- django/conf/locale/en/LC_MESSAGES/django.po | 18 ++--- .../admin/locale/en/LC_MESSAGES/django.po | 76 ++++++++++--------- .../auth/locale/en/LC_MESSAGES/django.po | 36 ++++----- 3 files changed, 68 insertions(+), 62 deletions(-) diff --git a/django/conf/locale/en/LC_MESSAGES/django.po b/django/conf/locale/en/LC_MESSAGES/django.po index 0ca6f0060794..faafa8111509 100644 --- a/django/conf/locale/en/LC_MESSAGES/django.po +++ b/django/conf/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-20 15:53-0400\n" +"POT-Creation-Date: 2026-07-22 16:14-0400\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -589,11 +589,11 @@ msgstr "" msgid "Null characters are not allowed." msgstr "" -#: db/models/base.py:1656 forms/models.py:933 +#: db/models/base.py:1664 forms/models.py:933 msgid "and" msgstr "" -#: db/models/base.py:1658 +#: db/models/base.py:1666 #, python-format msgid "%(model_name)s with this %(field_labels)s already exists." msgstr "" @@ -1043,27 +1043,27 @@ msgid "" "may be ambiguous or it may not exist." msgstr "" -#: forms/widgets.py:553 +#: forms/widgets.py:573 msgid "Clear" msgstr "" -#: forms/widgets.py:554 +#: forms/widgets.py:574 msgid "Currently" msgstr "" -#: forms/widgets.py:555 +#: forms/widgets.py:575 msgid "Change" msgstr "" -#: forms/widgets.py:894 +#: forms/widgets.py:914 msgid "Unknown" msgstr "" -#: forms/widgets.py:895 +#: forms/widgets.py:915 msgid "Yes" msgstr "" -#: forms/widgets.py:896 +#: forms/widgets.py:916 msgid "No" msgstr "" diff --git a/django/contrib/admin/locale/en/LC_MESSAGES/django.po b/django/contrib/admin/locale/en/LC_MESSAGES/django.po index 9c348886f360..1b7f4caaebe2 100644 --- a/django/contrib/admin/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/admin/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-20 15:53-0400\n" +"POT-Creation-Date: 2026-07-22 16:14-0400\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -24,7 +24,7 @@ msgstr "" msgid "Successfully deleted %(count)d %(items)s." msgstr "" -#: contrib/admin/actions.py:63 contrib/admin/options.py:2528 +#: contrib/admin/actions.py:63 contrib/admin/options.py:2535 #, python-format msgid "Cannot delete %(name)s" msgstr "" @@ -192,7 +192,7 @@ msgstr "" msgid "Added." msgstr "" -#: contrib/admin/models.py:149 contrib/admin/options.py:2785 +#: contrib/admin/models.py:149 contrib/admin/options.py:2818 msgid "and" msgstr "" @@ -215,122 +215,122 @@ msgstr "" msgid "No fields changed." msgstr "" -#: contrib/admin/options.py:293 contrib/admin/options.py:337 +#: contrib/admin/options.py:300 contrib/admin/options.py:344 msgid "None" msgstr "" -#: contrib/admin/options.py:389 +#: contrib/admin/options.py:396 msgid "Hold down “Control”, or “Command” on a Mac, to select more than one." msgstr "" -#: contrib/admin/options.py:1066 +#: contrib/admin/options.py:1073 #, python-brace-format msgid "Select this object for an action - {}" msgstr "" -#: contrib/admin/options.py:1597 +#: contrib/admin/options.py:1604 #, python-format msgid "The app \"%s\" could not be found." msgstr "" -#: contrib/admin/options.py:1639 contrib/admin/options.py:1677 +#: contrib/admin/options.py:1646 contrib/admin/options.py:1684 #, python-brace-format msgid "The {name} “{obj}” was added successfully." msgstr "" -#: contrib/admin/options.py:1641 +#: contrib/admin/options.py:1648 msgid "You may edit it again below." msgstr "" -#: contrib/admin/options.py:1658 +#: contrib/admin/options.py:1665 #, python-brace-format msgid "" "The {name} “{obj}” was added successfully. You may add another {name} below." msgstr "" -#: contrib/admin/options.py:1729 +#: contrib/admin/options.py:1736 #, python-brace-format msgid "" "The {name} “{obj}” was changed successfully. You may edit it again below." msgstr "" -#: contrib/admin/options.py:1749 +#: contrib/admin/options.py:1756 #, python-brace-format msgid "" "The {name} “{obj}” was changed successfully. You may add another {name} " "below." msgstr "" -#: contrib/admin/options.py:1771 +#: contrib/admin/options.py:1778 #, python-brace-format msgid "The {name} “{obj}” was changed successfully." msgstr "" -#: contrib/admin/options.py:1875 contrib/admin/options.py:2359 +#: contrib/admin/options.py:1882 contrib/admin/options.py:2366 msgid "" "Items must be selected in order to perform actions on them. No items have " "been changed." msgstr "" -#: contrib/admin/options.py:1895 +#: contrib/admin/options.py:1902 msgid "No action selected." msgstr "" -#: contrib/admin/options.py:1926 +#: contrib/admin/options.py:1933 #, python-format msgid "The %(name)s “%(obj)s” was deleted successfully." msgstr "" -#: contrib/admin/options.py:2028 +#: contrib/admin/options.py:2035 #, python-format msgid "%(name)s with ID “%(key)s” doesn’t exist. Perhaps it was deleted?" msgstr "" -#: contrib/admin/options.py:2181 +#: contrib/admin/options.py:2188 #, python-format msgid "Add %s" msgstr "" -#: contrib/admin/options.py:2183 +#: contrib/admin/options.py:2190 #, python-format msgid "Change %s" msgstr "" -#: contrib/admin/options.py:2185 +#: contrib/admin/options.py:2192 #, python-format msgid "View %s" msgstr "" -#: contrib/admin/options.py:2288 +#: contrib/admin/options.py:2295 #, python-format msgid "%(count)s %(name)s was changed successfully." msgid_plural "%(count)s %(name)s were changed successfully." msgstr[0] "" msgstr[1] "" -#: contrib/admin/options.py:2321 +#: contrib/admin/options.py:2328 msgid "Database error" msgstr "" -#: contrib/admin/options.py:2435 +#: contrib/admin/options.py:2442 #, python-format msgid "%(total_count)s selected" msgid_plural "All %(total_count)s selected" msgstr[0] "" msgstr[1] "" -#: contrib/admin/options.py:2441 +#: contrib/admin/options.py:2448 #, python-format msgid "0 of %(cnt)s selected" msgstr "" -#: contrib/admin/options.py:2530 +#: contrib/admin/options.py:2537 #: contrib/admin/templates/admin/delete_confirmation.html:18 #: contrib/admin/templates/admin/submit_line.html:14 msgid "Delete" msgstr "" -#: contrib/admin/options.py:2588 +#: contrib/admin/options.py:2595 #, python-format msgid "Change history: %s" msgstr "" @@ -338,12 +338,25 @@ msgstr "" #. Translators: Model verbose name and instance #. representation, suitable to be an item in a #. list. -#: contrib/admin/options.py:2779 +#: contrib/admin/options.py:2794 #, python-format msgid "%(class_name)s %(instance)s" msgstr "" -#: contrib/admin/options.py:2788 +#. Translators: This string is used as a +#. separator between list elements. +#: contrib/admin/options.py:2808 contrib/admin/options.py:2809 +msgid ", " +msgstr "" + +#: contrib/admin/options.py:2811 contrib/admin/templatetags/admin_filters.py:72 +#, python-format +msgid "…and %(count)d more object." +msgid_plural "…and %(count)d more objects." +msgstr[0] "" +msgstr[1] "" + +#: contrib/admin/options.py:2821 #, python-format msgid "" "Deleting %(class_name)s %(instance)s would require deleting the following " @@ -965,13 +978,6 @@ msgstr "" msgid "Reset my password" msgstr "" -#: contrib/admin/templatetags/admin_filters.py:72 -#, python-format -msgid "…and %(count)d more object." -msgid_plural "…and %(count)d more objects." -msgstr[0] "" -msgstr[1] "" - #: contrib/admin/templatetags/admin_list.py:104 msgid "Select all objects on this page for an action" msgstr "" diff --git a/django/contrib/auth/locale/en/LC_MESSAGES/django.po b/django/contrib/auth/locale/en/LC_MESSAGES/django.po index 2e1518e2ad72..e2a34b77b7e1 100644 --- a/django/contrib/auth/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/auth/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-20 15:53-0400\n" +"POT-Creation-Date: 2026-07-22 16:14-0400\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -149,56 +149,56 @@ msgstr "" msgid "Old password" msgstr "" -#: contrib/auth/hashers.py:357 contrib/auth/hashers.py:450 -#: contrib/auth/hashers.py:541 contrib/auth/hashers.py:636 -#: contrib/auth/hashers.py:689 +#: contrib/auth/hashers.py:355 contrib/auth/hashers.py:448 +#: contrib/auth/hashers.py:539 contrib/auth/hashers.py:634 +#: contrib/auth/hashers.py:685 msgid "algorithm" msgstr "" -#: contrib/auth/hashers.py:358 +#: contrib/auth/hashers.py:356 msgid "iterations" msgstr "" -#: contrib/auth/hashers.py:359 contrib/auth/hashers.py:456 -#: contrib/auth/hashers.py:543 contrib/auth/hashers.py:640 -#: contrib/auth/hashers.py:690 +#: contrib/auth/hashers.py:357 contrib/auth/hashers.py:454 +#: contrib/auth/hashers.py:541 contrib/auth/hashers.py:638 +#: contrib/auth/hashers.py:686 msgid "salt" msgstr "" -#: contrib/auth/hashers.py:360 contrib/auth/hashers.py:457 -#: contrib/auth/hashers.py:641 contrib/auth/hashers.py:691 +#: contrib/auth/hashers.py:358 contrib/auth/hashers.py:455 +#: contrib/auth/hashers.py:639 contrib/auth/hashers.py:687 msgid "hash" msgstr "" -#: contrib/auth/hashers.py:451 +#: contrib/auth/hashers.py:449 msgid "variety" msgstr "" -#: contrib/auth/hashers.py:452 +#: contrib/auth/hashers.py:450 msgid "version" msgstr "" -#: contrib/auth/hashers.py:453 +#: contrib/auth/hashers.py:451 msgid "memory cost" msgstr "" -#: contrib/auth/hashers.py:454 +#: contrib/auth/hashers.py:452 msgid "time cost" msgstr "" -#: contrib/auth/hashers.py:455 contrib/auth/hashers.py:639 +#: contrib/auth/hashers.py:453 contrib/auth/hashers.py:637 msgid "parallelism" msgstr "" -#: contrib/auth/hashers.py:542 contrib/auth/hashers.py:637 +#: contrib/auth/hashers.py:540 contrib/auth/hashers.py:635 msgid "work factor" msgstr "" -#: contrib/auth/hashers.py:544 +#: contrib/auth/hashers.py:542 msgid "checksum" msgstr "" -#: contrib/auth/hashers.py:638 +#: contrib/auth/hashers.py:636 msgid "block size" msgstr "" From 50c2b7c83661a61da48f78dd0130fc3cbf8ed39f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 23 Jul 2026 11:32:18 -0400 Subject: [PATCH 5/5] Refs #36694 -- Skipped GIS test needing spatial index on Oracle. --- tests/gis_tests/geoapp/test_indexes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/gis_tests/geoapp/test_indexes.py b/tests/gis_tests/geoapp/test_indexes.py index 2da0070e30a7..5f0f610e8f0d 100644 --- a/tests/gis_tests/geoapp/test_indexes.py +++ b/tests/gis_tests/geoapp/test_indexes.py @@ -96,6 +96,8 @@ def test_partial_index(self): @skipUnlessDBFeature("supports_tablespaces") def test_tablespace(self): + if not self.has_spatial_indexes(City._meta.db_table): + self.skipTest("Spatial indexes in Meta.indexes are not supported.") index_name = "city_point_partial_tblspce_idx" index = Index( name=index_name,