diff --git a/wagtail/actions/publish_revision.py b/wagtail/actions/publish_revision.py
index 15da5ec9fcfb..79c07df7d565 100644
--- a/wagtail/actions/publish_revision.py
+++ b/wagtail/actions/publish_revision.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
from django.conf import settings
from django.core.exceptions import PermissionDenied
@@ -47,7 +47,7 @@ def __init__(
user=None,
changed: bool = True,
log_action: bool = True,
- previous_revision: Optional[Revision] = None,
+ previous_revision: Revision | None = None,
):
self.revision = revision
self.object = self.revision.as_object()
@@ -107,7 +107,7 @@ def _publish_revision(
user,
changed,
log_action: bool,
- previous_revision: Optional[Revision] = None,
+ previous_revision: Revision | None = None,
):
from wagtail.models import Revision
diff --git a/wagtail/admin/panels/model_utils.py b/wagtail/admin/panels/model_utils.py
index 95c8f2927a1f..8858a528e462 100644
--- a/wagtail/admin/panels/model_utils.py
+++ b/wagtail/admin/panels/model_utils.py
@@ -34,7 +34,7 @@ def extract_panel_definitions_from_model_class(model, exclude=None):
return panels
-@functools.lru_cache(maxsize=None)
+@functools.cache
def get_edit_handler(model):
"""
Get the panel to use in the Wagtail admin when editing this model.
diff --git a/wagtail/admin/tests/test_edit_handlers.py b/wagtail/admin/tests/test_edit_handlers.py
index efeeefc33db9..cc030fdd3479 100644
--- a/wagtail/admin/tests/test_edit_handlers.py
+++ b/wagtail/admin/tests/test_edit_handlers.py
@@ -1,6 +1,7 @@
+from collections.abc import Mapping
from datetime import date, datetime, timezone
from functools import wraps
-from typing import Any, List, Mapping, Optional
+from typing import Any, Optional
from unittest import mock
from django import forms
@@ -846,7 +847,7 @@ def setUp(self):
def _get_form(
self,
data: Optional[Mapping[str, Any]] = None,
- fields: Optional[List[str]] = None,
+ fields: Optional[list[str]] = None,
) -> WagtailAdminPageForm:
cls = get_form_for_model(
EventPage,
diff --git a/wagtail/admin/ui/sidebar.py b/wagtail/admin/ui/sidebar.py
index 17d42699af70..5305b256c9df 100644
--- a/wagtail/admin/ui/sidebar.py
+++ b/wagtail/admin/ui/sidebar.py
@@ -1,4 +1,5 @@
-from typing import Any, List, Mapping
+from collections.abc import Mapping
+from typing import Any
from warnings import warn
from django import forms
@@ -152,7 +153,7 @@ def __init__(
self,
name: str,
label: str,
- menu_items: List[MenuItem],
+ menu_items: list[MenuItem],
icon_name: str = "",
classname: str = "",
classnames: str = "",
@@ -262,7 +263,7 @@ def js_args(self):
@adapter("wagtail.sidebar.MainMenuModule", base=BaseSidebarAdapter)
class MainMenuModule:
def __init__(
- self, menu_items: List[MenuItem], account_menu_items: List[MenuItem], user
+ self, menu_items: list[MenuItem], account_menu_items: list[MenuItem], user
):
self.menu_items = menu_items
self.account_menu_items = account_menu_items
diff --git a/wagtail/admin/views/generic/base.py b/wagtail/admin/views/generic/base.py
index 45d40ddd685a..185b82b2ca21 100644
--- a/wagtail/admin/views/generic/base.py
+++ b/wagtail/admin/views/generic/base.py
@@ -317,7 +317,7 @@ def active_filters(self):
ActiveFilter(
bound_field.auto_id,
filter_def.label,
- "%s - %s" % (start_date_display, end_date_display),
+ f"{start_date_display} - {end_date_display}",
self.get_url_without_filter_param(
[
widget.suffixed(field_name, suffix)
diff --git a/wagtail/admin/views/home.py b/wagtail/admin/views/home.py
index 070ff16e1c1e..b88a6b0c75fc 100644
--- a/wagtail/admin/views/home.py
+++ b/wagtail/admin/views/home.py
@@ -1,4 +1,5 @@
-from typing import Any, Mapping, Union
+from collections.abc import Mapping
+from typing import Any, Union
from django.conf import settings
from django.contrib.auth import get_user_model
diff --git a/wagtail/admin/views/pages/search.py b/wagtail/admin/views/pages/search.py
index f684d09340ee..e4d39a0449e2 100644
--- a/wagtail/admin/views/pages/search.py
+++ b/wagtail/admin/views/pages/search.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
@@ -165,7 +165,7 @@ def get_table_kwargs(self):
kwargs["actions_next_url"] = self.get_index_url()
return kwargs
- def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context.update(
{
@@ -182,7 +182,7 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
class SearchView(BaseSearchView):
template_name = "wagtailadmin/pages/search.html"
- def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["search_form"] = SearchForm(self.request.GET)
return context
diff --git a/wagtail/admin/views/pages/usage.py b/wagtail/admin/views/pages/usage.py
index a9e2d0da4cf0..801df35617f8 100644
--- a/wagtail/admin/views/pages/usage.py
+++ b/wagtail/admin/views/pages/usage.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
@@ -68,7 +68,7 @@ def get_index_url(self):
],
)
- def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["page_class"] = self.page_class
return context
diff --git a/wagtail/contrib/redirects/signal_handlers.py b/wagtail/contrib/redirects/signal_handlers.py
index ffc18465840d..13172596a70b 100644
--- a/wagtail/contrib/redirects/signal_handlers.py
+++ b/wagtail/contrib/redirects/signal_handlers.py
@@ -1,5 +1,5 @@
import logging
-from typing import Iterable, Set, Tuple
+from collections.abc import Iterable
from django.conf import settings
from django.db.models import Q
@@ -89,8 +89,8 @@ def autocreate_redirects_on_page_move(
def _page_urls_for_sites(
- page: Page, sites: Tuple[Site], cache_target: Page
-) -> Set[Tuple[Site, str, str]]:
+ page: Page, sites: tuple[Site], cache_target: Page
+) -> set[tuple[Site, str, str]]:
urls = set()
for site in sites:
# use a `HttpRequest` to influence the return value
diff --git a/wagtail/contrib/redirects/views.py b/wagtail/contrib/redirects/views.py
index b64bd3177131..35394a1a3e38 100644
--- a/wagtail/contrib/redirects/views.py
+++ b/wagtail/contrib/redirects/views.py
@@ -1,5 +1,4 @@
import os
-from typing import List
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.db import transaction
@@ -123,7 +122,7 @@ def get_base_queryset(self):
return super().get_base_queryset().select_related("redirect_page", "site")
@cached_property
- def header_more_buttons(self) -> List[Button]:
+ def header_more_buttons(self) -> list[Button]:
buttons = super().header_more_buttons.copy()
buttons.append(
Button(
diff --git a/wagtail/contrib/simple_translation/wagtail_hooks.py b/wagtail/contrib/simple_translation/wagtail_hooks.py
index 443ead7fd962..1b8248d0b6f3 100644
--- a/wagtail/contrib/simple_translation/wagtail_hooks.py
+++ b/wagtail/contrib/simple_translation/wagtail_hooks.py
@@ -1,5 +1,3 @@
-from typing import List
-
from django.conf import settings
from django.contrib.admin.utils import quote
from django.contrib.auth.models import Permission
@@ -118,7 +116,7 @@ def register_snippet_listing_buttons(snippet, user, next_url=None):
@hooks.register("construct_translated_pages_to_cascade_actions")
-def construct_translated_pages_to_cascade_actions(pages: List[Page], action: str):
+def construct_translated_pages_to_cascade_actions(pages: list[Page], action: str):
if not getattr(settings, "WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE", False):
return
diff --git a/wagtail/coreutils.py b/wagtail/coreutils.py
index be7d065d2fff..523a85e351c5 100644
--- a/wagtail/coreutils.py
+++ b/wagtail/coreutils.py
@@ -3,8 +3,9 @@
import logging
import re
import unicodedata
+from collections.abc import Iterable
from hashlib import md5
-from typing import TYPE_CHECKING, Any, Dict, Iterable, Union
+from typing import TYPE_CHECKING, Any, Union
from warnings import warn
from anyascii import anyascii
@@ -256,7 +257,7 @@ def find_available_slug(parent, requested_slug, ignore_page_id=None):
return slug
-@functools.lru_cache(maxsize=None)
+@functools.cache
def get_content_languages():
"""
Cache of settings.WAGTAIL_CONTENT_LANGUAGES in a dictionary for easy lookups by key.
@@ -545,7 +546,7 @@ def add(self, *, instance: Model = None, **kwargs) -> None:
if self.max_size and len(self.items) == self.max_size:
self.process()
- def extend(self, iterable: Iterable[Union[Model, Dict[str, Any]]]) -> None:
+ def extend(self, iterable: Iterable[Union[Model, dict[str, Any]]]) -> None:
for value in iterable:
if isinstance(value, self.model):
self.add(instance=value)
diff --git a/wagtail/documents/rich_text/__init__.py b/wagtail/documents/rich_text/__init__.py
index 3dd4df5b33d6..1d95b1823c96 100644
--- a/wagtail/documents/rich_text/__init__.py
+++ b/wagtail/documents/rich_text/__init__.py
@@ -1,5 +1,3 @@
-from typing import List
-
from django.utils.html import escape
from wagtail.documents import get_document_model
@@ -20,7 +18,7 @@ def expand_db_attributes(cls, attrs: dict) -> str:
return cls.expand_db_attributes_many([attrs])[0]
@classmethod
- def expand_db_attributes_many(cls, attrs_list: List[dict]) -> List[str]:
+ def expand_db_attributes_many(cls, attrs_list: list[dict]) -> list[str]:
return [
'' % escape(doc.url) if doc else ""
for doc in cls.get_many(attrs_list)
diff --git a/wagtail/images/models.py b/wagtail/images/models.py
index 4efaf145fac1..3c7d6bf72b6e 100644
--- a/wagtail/images/models.py
+++ b/wagtail/images/models.py
@@ -8,10 +8,11 @@
import re
import time
from collections import OrderedDict, defaultdict
+from collections.abc import Iterable
from contextlib import contextmanager
from io import BytesIO
from tempfile import SpooledTemporaryFile
-from typing import Any, Dict, Iterable, List, Optional, Union
+from typing import Any
import willow
from django.apps import apps
@@ -442,12 +443,12 @@ def get_rendition_model(cls):
"""Get the Rendition model for this Image model"""
return cls.renditions.rel.related_model
- def _get_prefetched_renditions(self) -> Union[Iterable["AbstractRendition"], None]:
+ def _get_prefetched_renditions(self) -> Iterable[AbstractRendition] | None:
if "renditions" in getattr(self, "_prefetched_objects_cache", {}):
return self.renditions.all()
return getattr(self, "prefetched_renditions", None)
- def _add_to_prefetched_renditions(self, rendition: "AbstractRendition") -> None:
+ def _add_to_prefetched_renditions(self, rendition: AbstractRendition) -> None:
# Reuse this rendition if requested again from this object
try:
self._prefetched_objects_cache["renditions"]._result_cache.append(rendition)
@@ -458,7 +459,7 @@ def _add_to_prefetched_renditions(self, rendition: "AbstractRendition") -> None:
except AttributeError:
pass
- def get_rendition(self, filter: Union["Filter", str]) -> "AbstractRendition":
+ def get_rendition(self, filter: Filter | str) -> AbstractRendition:
"""
Returns a ``Rendition`` instance with a ``file`` field value (an
image) reflecting the supplied ``filter`` value and focal point values
@@ -486,7 +487,7 @@ def get_rendition(self, filter: Union["Filter", str]) -> "AbstractRendition":
return rendition
- def find_existing_rendition(self, filter: "Filter") -> "AbstractRendition":
+ def find_existing_rendition(self, filter: Filter) -> AbstractRendition:
"""
Returns an existing ``Rendition`` instance with a ``file`` field value
(an image) reflecting the supplied ``filter`` value and focal point
@@ -505,7 +506,7 @@ def find_existing_rendition(self, filter: "Filter") -> "AbstractRendition":
except KeyError:
raise Rendition.DoesNotExist
- def create_rendition(self, filter: "Filter") -> "AbstractRendition":
+ def create_rendition(self, filter: Filter) -> AbstractRendition:
"""
Creates and returns a ``Rendition`` instance with a ``file`` field
value (an image) reflecting the supplied ``filter`` value and focal
@@ -526,9 +527,7 @@ def create_rendition(self, filter: "Filter") -> "AbstractRendition":
)
return rendition
- def get_renditions(
- self, *filters: Union["Filter", str]
- ) -> Dict[str, "AbstractRendition"]:
+ def get_renditions(self, *filters: Filter | str) -> dict[str, AbstractRendition]:
"""
Returns a ``dict`` of ``Rendition`` instances with image files reflecting
the supplied ``filters``, keyed by filter spec patterns.
@@ -566,8 +565,8 @@ def get_renditions(
return {filter.spec: renditions[filter] for filter in filters}
def find_existing_renditions(
- self, *filters: "Filter"
- ) -> Dict["Filter", "AbstractRendition"]:
+ self, *filters: Filter
+ ) -> dict[Filter, AbstractRendition]:
"""
Returns a dictionary of existing ``Rendition`` instances with ``file``
values (images) reflecting the supplied ``filters`` and the focal point
@@ -578,8 +577,8 @@ def find_existing_renditions(
created before, the return value will be an empty dict.
"""
Rendition = self.get_rendition_model()
- filters_by_spec: Dict[str, Filter] = {f.spec: f for f in filters}
- found: Dict[Filter, AbstractRendition] = {}
+ filters_by_spec: dict[str, Filter] = {f.spec: f for f in filters}
+ found: dict[Filter, AbstractRendition] = {}
# Interrogate prefetched values first (where available)
prefetched_renditions = self._get_prefetched_renditions()
@@ -589,7 +588,7 @@ def find_existing_renditions(
# prefetched value, and further cache/database lookups are avoided.
# group renditions by the filters of interest
- potential_matches: Dict[Filter, List[AbstractRendition]] = defaultdict(list)
+ potential_matches: dict[Filter, list[AbstractRendition]] = defaultdict(list)
for rendition in prefetched_renditions:
try:
filter = filters_by_spec[rendition.filter_spec]
@@ -637,9 +636,7 @@ def find_existing_renditions(
found[filter] = rendition
return found
- def create_renditions(
- self, *filters: "Filter"
- ) -> Dict["Filter", "AbstractRendition"]:
+ def create_renditions(self, *filters: Filter) -> dict[Filter, AbstractRendition]:
"""
Creates multiple ``Rendition`` instances with image files reflecting the supplied
``filters``, and returns them as a ``dict`` keyed by the relevant ``Filter`` instance.
@@ -664,8 +661,8 @@ def create_renditions(
filter = filters[0]
return {filter: self.create_rendition(filter)}
- return_value: Dict[Filter, AbstractRendition] = {}
- filter_map: Dict[str, Filter] = {f.spec: f for f in filters}
+ return_value: dict[Filter, AbstractRendition] = {}
+ filter_map: dict[str, Filter] = {f.spec: f for f in filters}
# Read file contents into memory
with self.open_file() as file:
@@ -688,7 +685,7 @@ def create_renditions(
# identical renditions in the meantime, we should find them to avoid clashes.
# NB: Clashes can still occur, because there is no get_or_create() equivalent
# for multiple objects. However, this will reduce that risk considerably.
- files_for_deletion: List[File] = []
+ files_for_deletion: list[File] = []
# Assemble Q() to identify potential clashes
lookup_q = Q()
@@ -724,8 +721,8 @@ def create_renditions(
return return_value
def generate_rendition_instance(
- self, filter: "Filter", source: BytesIO
- ) -> "AbstractRendition":
+ self, filter: Filter, source: BytesIO
+ ) -> AbstractRendition:
"""
Use the supplied ``source`` image to create and return an
**unsaved** ``Rendition`` instance, with a ``file`` value reflecting
@@ -740,7 +737,7 @@ def generate_rendition_instance(
),
)
- def generate_rendition_file(self, filter: "Filter", *, source: File = None) -> File:
+ def generate_rendition_file(self, filter: Filter, *, source: File = None) -> File:
"""
Generates an in-memory image matching the supplied ``filter`` value
and focal point value from this object, wraps it in a ``File`` object
@@ -877,7 +874,7 @@ def __init__(self, spec=None):
self.spec = spec
@classmethod
- def expand_spec(self, spec: Union["str", Iterable["str"]]) -> List["str"]:
+ def expand_spec(self, spec: str | Iterable[str]) -> list[str]:
"""
Converts a spec pattern with brace-expansions, into a list of spec patterns.
For example, "width-{100,200}" becomes ["width-100", "width-200"].
@@ -1092,14 +1089,14 @@ class ResponsiveImage:
def __init__(
self,
- renditions: Dict[str, "AbstractRendition"],
- attrs: Optional[Dict[str, Any]] = None,
+ renditions: dict[str, AbstractRendition],
+ attrs: dict[str, Any] | None = None,
):
self.renditions = list(renditions.values())
self.attrs = attrs
@classmethod
- def get_width_srcset(cls, renditions_list: List["AbstractRendition"]):
+ def get_width_srcset(cls, renditions_list: list[AbstractRendition]):
if len(renditions_list) == 1:
# No point in using width descriptors if there is a single image.
return renditions_list[0].url
@@ -1122,7 +1119,7 @@ def __str__(self):
def __bool__(self):
return bool(self.renditions)
- def __eq__(self, other: "ResponsiveImage"):
+ def __eq__(self, other: ResponsiveImage):
if isinstance(other, ResponsiveImage):
return self.renditions == other.renditions and self.attrs == other.attrs
return False
@@ -1138,16 +1135,16 @@ class Picture(ResponsiveImage):
def __init__(
self,
- renditions: Dict[str, "AbstractRendition"],
- attrs: Optional[Dict[str, Any]] = None,
+ renditions: dict[str, AbstractRendition],
+ attrs: dict[str, Any] | None = None,
):
super().__init__(renditions, attrs)
# Store renditions grouped by format separately for access from templates.
self.formats = self.get_formats(renditions)
def get_formats(
- self, renditions: Dict[str, "AbstractRendition"]
- ) -> Dict[str, List["AbstractRendition"]]:
+ self, renditions: dict[str, AbstractRendition]
+ ) -> dict[str, list[AbstractRendition]]:
"""
Group renditions by the format they are for, if any.
If there is only one format, no grouping is required.
diff --git a/wagtail/images/rich_text/__init__.py b/wagtail/images/rich_text/__init__.py
index e3125ada0778..ea7730179822 100644
--- a/wagtail/images/rich_text/__init__.py
+++ b/wagtail/images/rich_text/__init__.py
@@ -1,5 +1,3 @@
-from typing import List
-
from wagtail.images import get_image_model
from wagtail.images.formats import get_image_format
from wagtail.rich_text import EmbedHandler
@@ -19,7 +17,7 @@ def expand_db_attributes(cls, attrs: dict) -> str:
return cls.expand_db_attributes_many([attrs])[0]
@classmethod
- def expand_db_attributes_many(cls, attrs_list: List[dict]) -> List[str]:
+ def expand_db_attributes_many(cls, attrs_list: list[dict]) -> list[str]:
"""
Given a dict of attributes from the