diff --git a/docs/baseframe/conf.py b/docs/baseframe/conf.py index 3b46ce81..1ae2cc58 100644 --- a/docs/baseframe/conf.py +++ b/docs/baseframe/conf.py @@ -15,7 +15,6 @@ import os import sys -import typing as t sys.path.append(os.path.abspath('../../src')) from baseframe import _version # isort:skip @@ -181,7 +180,7 @@ # -- Options for LaTeX output -------------------------------------------------- -latex_elements: t.Dict[str, str] = { +latex_elements: dict[str, str] = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). diff --git a/src/baseframe/blueprint.py b/src/baseframe/blueprint.py index 1688dda7..84fcbd85 100644 --- a/src/baseframe/blueprint.py +++ b/src/baseframe/blueprint.py @@ -4,8 +4,8 @@ import json import os.path -import typing as t -import typing_extensions as te +from collections.abc import Iterable +from typing import Literal, Optional, Union import sentry_sdk from flask import Blueprint, Flask @@ -59,7 +59,7 @@ } -def _select_jinja_autoescape(filename: t.Optional[str]) -> bool: +def _select_jinja_autoescape(filename: Optional[str]) -> bool: """Return `True` if autoescaping should be active for the given template name.""" if filename is None: return False @@ -83,12 +83,12 @@ class BaseframeBlueprint(Blueprint): def init_app( self, app: Flask, - requires: t.Iterable[str] = (), - ext_requires: t.Iterable[t.Union[str, t.List[str], t.Tuple[str, ...]]] = (), - bundle_js: t.Optional[Bundle] = None, - bundle_css: t.Optional[Bundle] = None, - assetenv: t.Optional[Environment] = None, - theme: te.Literal['bootstrap3', 'mui'] = 'bootstrap3', + requires: Iterable[str] = (), + ext_requires: Iterable[Union[str, list[str], tuple[str, ...]]] = (), + bundle_js: Optional[Bundle] = None, + bundle_css: Optional[Bundle] = None, + assetenv: Optional[Environment] = None, + theme: Literal['bootstrap3', 'mui'] = 'bootstrap3', error_handlers: bool = True, ) -> None: """ @@ -181,10 +181,10 @@ def init_app( else: subdomain = None - ignore_js: t.List[str] = [] - ignore_css: t.List[str] = [] - ext_js: t.List[t.List[str]] = [] - ext_css: t.List[t.List[str]] = [] + ignore_js: list[str] = [] + ignore_css: list[str] = [] + ext_js: list[list[str]] = [] + ext_css: list[list[str]] = [] requires = [ item for itemgroup in ext_requires @@ -196,8 +196,8 @@ def init_app( app.config['ext_js'] = ext_js app.config['ext_css'] = ext_css - assets_js: t.List[str] = [] - assets_css: t.List[str] = [] + assets_js: list[str] = [] + assets_css: list[str] = [] for item in requires: name, spec = split_namespec(item) for alist, ext in [(assets_js, '.js'), (assets_css, '.css')]: diff --git a/src/baseframe/extensions.py b/src/baseframe/extensions.py index 9618272d..bf25943e 100644 --- a/src/baseframe/extensions.py +++ b/src/baseframe/extensions.py @@ -3,10 +3,9 @@ # pyright: reportMissingImports = false import os.path -import typing as t -import typing_extensions as te from datetime import tzinfo -from typing import cast +from typing import Union, cast +from typing_extensions import LiteralString, Protocol from flask import current_app, request from flask_babel import Babel, Domain @@ -39,7 +38,7 @@ ] -class GetTextProtocol(te.Protocol): +class GetTextProtocol(Protocol): """ Callable protocol for gettext and lazy_gettext. @@ -47,7 +46,7 @@ class GetTextProtocol(te.Protocol): and that the return type is a string (though actually a LazyString). """ - def __call__(self, string: te.LiteralString, **variables) -> str: ... + def __call__(self, string: LiteralString, **variables) -> str: ... DEFAULT_LOCALE = 'en' @@ -87,7 +86,7 @@ def get_user_locale() -> str: ) or DEFAULT_LOCALE -def get_timezone(default: t.Union[None, tzinfo, str] = None) -> tzinfo: +def get_timezone(default: Union[None, tzinfo, str] = None) -> tzinfo: """Return a timezone suitable for the current context.""" # If this app and request have a user, return user's timezone, # else return app default timezone diff --git a/src/baseframe/filters.py b/src/baseframe/filters.py index efca1f57..c0e7abcc 100644 --- a/src/baseframe/filters.py +++ b/src/baseframe/filters.py @@ -1,10 +1,8 @@ """Jinja2 filters.""" import os.path -import typing as t -import typing_extensions as te from datetime import date, datetime, time, timedelta -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast from urllib.parse import urlsplit, urlunsplit import grapheme @@ -60,7 +58,7 @@ def age(dt: datetime) -> str: @baseframe.app_template_filter('initials') -def initials(text: t.Optional[str]) -> str: +def initials(text: Optional[str]) -> str: """Return up to two initials from the given string, for a default avatar image.""" if not text: return '' @@ -101,10 +99,8 @@ def nossl(url: str) -> str: # TODO: Move this into Hasjob as it's not used elsewhere @baseframe.app_template_filter('avatar_url') def avatar_url( - user: t.Any, - size: t.Optional[ - t.Union[str, t.List[int], t.Tuple[int, int], t.Tuple[str, str]] - ] = None, + user: Any, + size: Optional[Union[str, list[int], tuple[int, int], tuple[str, str]]] = None, ) -> str: """Generate an avatar for the given user.""" if isinstance(size, (list, tuple)): @@ -135,7 +131,7 @@ def avatar_url( @baseframe.app_template_filter('render_field_options') -def render_field_options(field: WTField, **kwargs: t.Any) -> str: +def render_field_options(field: WTField, **kwargs: Any) -> str: """Remove HTML attributes with falsy values before rendering a field.""" d = {k: v for k, v in kwargs.items() if v is not None and v is not False} if field.render_kw: @@ -145,9 +141,9 @@ def render_field_options(field: WTField, **kwargs: t.Any) -> str: # TODO: Only used in renderfield.mustache. Re-check whether this is necessary at all. @baseframe.app_template_filter('to_json') -def form_field_to_json(field: WTField, **kwargs: t.Any) -> t.Dict[str, t.Any]: +def form_field_to_json(field: WTField, **kwargs: Any) -> dict[str, Any]: """Render a form field as JSON.""" - d: t.Dict[str, t.Any] = {} + d: dict[str, Any] = {} d['id'] = field.id d['label'] = field.label.text d['has_errors'] = bool(field.errors) @@ -171,7 +167,7 @@ def field_markdown(text: str) -> Markup: @baseframe.app_template_filter('ext_asset_url') -def ext_asset_url(asset: t.Union[str, t.List[str]]) -> str: +def ext_asset_url(asset: Union[str, list[str]]) -> str: """Return external asset URL for use in templates.""" if isinstance(asset, str): return ext_assets([asset]) @@ -232,10 +228,10 @@ def cdata(text: str) -> str: # TODO: Used only in Hasjob. Move there? @baseframe.app_template_filter('shortdate') -def shortdate(value: t.Union[datetime, date]) -> str: +def shortdate(value: Union[datetime, date]) -> str: """Render a date in short form (deprecated for lack of i18n support).""" - dt: t.Union[datetime, date] - utc_now: t.Union[datetime, date] + dt: Union[datetime, date] + utc_now: Union[datetime, date] if isinstance(value, datetime): tz = get_timezone() if value.tzinfo is None: @@ -258,9 +254,9 @@ def shortdate(value: t.Union[datetime, date]) -> str: # TODO: Only used in Hasjob. Move there? @baseframe.app_template_filter('longdate') -def longdate(value: t.Union[datetime, date]) -> str: +def longdate(value: Union[datetime, date]) -> str: """Render a date in long form (deprecated for lack of i18n support).""" - dt: t.Union[datetime, date] + dt: Union[datetime, date] if isinstance(value, datetime): if value.tzinfo is None: dt = utc.localize(value).astimezone(get_timezone()) @@ -273,13 +269,13 @@ def longdate(value: t.Union[datetime, date]) -> str: @baseframe.app_template_filter('date') def date_filter( - value: t.Union[datetime, date], + value: Union[datetime, date], format: str = 'medium', # noqa: A002 # pylint: disable=W0622 - locale: t.Optional[t.Union[Locale, str]] = None, + locale: Optional[Union[Locale, str]] = None, usertz: bool = True, ) -> str: """Render a localized date.""" - dt: t.Union[datetime, date] + dt: Union[datetime, date] if isinstance(value, datetime) and usertz: if value.tzinfo is None: dt = utc.localize(value).astimezone(get_timezone()) @@ -294,14 +290,14 @@ def date_filter( @baseframe.app_template_filter('time') def time_filter( - value: t.Union[datetime, time], + value: Union[datetime, time], format: str = 'short', # noqa: A002 # pylint: disable=W0622 - locale: t.Optional[t.Union[Locale, str]] = None, + locale: Optional[Union[Locale, str]] = None, usertz: bool = True, ) -> str: """Render a localized time.""" # Default format = hh:mm - dt: t.Union[datetime, time] + dt: Union[datetime, time] if isinstance(value, datetime) and usertz: if value.tzinfo is None: dt = utc.localize(value).astimezone(get_timezone()) @@ -316,13 +312,13 @@ def time_filter( @baseframe.app_template_filter('datetime') def datetime_filter( - value: t.Union[datetime, date, time], + value: Union[datetime, date, time], format: str = 'medium', # noqa: A002 # pylint: disable=W0622 - locale: t.Optional[t.Union[Locale, str]] = None, + locale: Optional[Union[Locale, str]] = None, usertz: bool = True, ) -> str: """Render a localized date and time.""" - dt: t.Union[datetime, date, time] + dt: Union[datetime, date, time] if isinstance(value, datetime) and usertz: if value.tzinfo is None: dt = utc.localize(value).astimezone(get_timezone()) @@ -345,16 +341,16 @@ def timestamp_filter(value: datetime) -> float: @baseframe.app_template_filter('timedelta') def timedelta_filter( - delta: t.Union[int, timedelta, datetime], - granularity: te.Literal[ + delta: Union[int, timedelta, datetime], + granularity: Literal[ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second' ] = 'second', threshold: float = 0.85, add_direction: bool = False, - format: te.Literal[ # noqa: A002 # pylint: disable=W0622 + format: Literal[ # noqa: A002 # pylint: disable=W0622 'narrow', 'short', 'medium', 'long' ] = 'long', - locale: t.Optional[t.Union[Locale, str]] = None, + locale: Optional[Union[Locale, str]] = None, ) -> str: """ Render a timedelta or int (representing seconds) as a duration. @@ -390,7 +386,7 @@ def timedelta_filter( @baseframe.app_template_filter('cleanurl') -def cleanurl_filter(url: t.Union[str, furl]) -> furl: +def cleanurl_filter(url: Union[str, furl]) -> furl: """Clean a URL visually by removing defaults like scheme and the ``www`` prefix.""" if not isinstance(url, furl): url = furl(url) diff --git a/src/baseframe/forms/auto.py b/src/baseframe/forms/auto.py index 5fe7f6ce..9acc7738 100644 --- a/src/baseframe/forms/auto.py +++ b/src/baseframe/forms/auto.py @@ -2,7 +2,7 @@ from __future__ import annotations -import typing as t +from typing import TYPE_CHECKING, Any, Optional, Union import wtforms from flask import ( @@ -26,7 +26,7 @@ from .fields import SubmitField from .form import Form -if t.TYPE_CHECKING: +if TYPE_CHECKING: from flask_sqlalchemy import SQLAlchemy _submit_str = __("Submit") @@ -43,15 +43,15 @@ class ConfirmDeleteForm(Form): def render_form( form: Form, title: str, - message: t.Optional[t.Union[str, Markup]] = None, - formid: t.Optional[str] = None, + message: Optional[Union[str, Markup]] = None, + formid: Optional[str] = None, submit: str = _submit_str, - cancel_url: t.Optional[str] = None, + cancel_url: Optional[str] = None, ajax: bool = False, with_chrome: bool = True, - action: t.Optional[str] = None, + action: Optional[str] = None, autosave: bool = False, - draft_revision: t.Optional[t.Any] = None, + draft_revision: Optional[Any] = None, template: str = '', ) -> Response: """Render a form.""" @@ -123,15 +123,15 @@ def render_redirect(url: str, code: int = 302) -> Response: def render_delete_sqla( - obj: t.Any, + obj: Any, db: SQLAlchemy, title: str, message: str, success: str = '', - next: t.Optional[str] = None, # noqa: A002 # pylint: disable=W0622 - cancel_url: t.Optional[str] = None, - delete_text: t.Optional[str] = None, - cancel_text: t.Optional[str] = None, + next: Optional[str] = None, # noqa: A002 # pylint: disable=W0622 + cancel_url: Optional[str] = None, + delete_text: Optional[str] = None, + cancel_text: Optional[str] = None, ) -> Response: """Render a delete page for SQLAlchemy objects.""" if not obj: diff --git a/src/baseframe/forms/fields.py b/src/baseframe/forms/fields.py index 413deef1..f8409692 100644 --- a/src/baseframe/forms/fields.py +++ b/src/baseframe/forms/fields.py @@ -5,11 +5,11 @@ from __future__ import annotations import itertools -import typing as t -import typing_extensions as te -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from datetime import datetime, tzinfo from decimal import Decimal, InvalidOperation as DecimalError +from typing import Any, Callable, Optional, Union, cast, runtime_checkable +from typing_extensions import Protocol from urllib.parse import urljoin import bleach @@ -104,8 +104,8 @@ SANITIZE_ATTRIBUTES = {'a': ['href', 'title', 'target']} -@te.runtime_checkable -class GeonameidProtocol(te.Protocol): +@runtime_checkable +class GeonameidProtocol(Protocol): geonameid: str @@ -115,8 +115,8 @@ class RecaptchaField(RecaptchaFieldBase): def __init__( self, label: str = '', - validators: t.Optional[ValidatorList] = None, - **kwargs: t.Any, + validators: Optional[ValidatorList] = None, + **kwargs: Any, ) -> None: validators = validators or [Recaptcha()] super().__init__(label, validators, **kwargs) @@ -170,19 +170,19 @@ def pre_validate(self, form: WTForm) -> None: class TinyMce4Field(TextAreaField): """Rich text field using TinyMCE 4.""" - data: t.Optional[str] + data: Optional[str] widget = TinyMce4() def __init__( self, - *args: t.Any, - content_css: t.Optional[t.Union[str, t.Callable[[], str]]] = None, + *args: Any, + content_css: Optional[Union[str, Callable[[], str]]] = None, linkify: bool = True, nofollow: bool = True, - tinymce_options: t.Optional[t.Dict[str, t.Any]] = None, - sanitize_tags: t.Optional[t.List[str]] = None, - sanitize_attributes: t.Optional[t.Dict[str, t.List[str]]] = None, - **kwargs: t.Any, + tinymce_options: Optional[dict[str, Any]] = None, + sanitize_tags: Optional[list[str]] = None, + sanitize_attributes: Optional[dict[str, list[str]]] = None, + **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) @@ -234,12 +234,12 @@ def __init__( self.sanitize_attributes = sanitize_attributes @property - def content_css(self) -> t.Optional[str]: + def content_css(self) -> Optional[str]: if callable(self._content_css): return self._content_css() return self._content_css - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" super().process_formdata(valuelist) # Sanitize data @@ -274,18 +274,18 @@ class DateTimeField(DateTimeFieldBase): """ widget = DateTimeInput() - data: t.Optional[datetime] + data: Optional[datetime] default_message = __("This date/time could not be recognized") _timezone: tzinfo def __init__( self, - *args: t.Any, + *args: Any, display_format: str = '%Y-%m-%dT%H:%M', - timezone: t.Union[str, tzinfo, None] = None, - message: t.Optional[str] = None, + timezone: Union[str, tzinfo, None] = None, + message: Optional[str] = None, naive: bool = True, - **kwargs: t.Any, + **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.display_format = display_format @@ -298,7 +298,7 @@ def timezone(self) -> tzinfo: return self._timezone @timezone.setter - def timezone(self, value: t.Union[str, tzinfo, None]) -> None: + def timezone(self, value: Union[str, tzinfo, None]) -> None: if value is None: value = get_timezone() if isinstance(value, str): @@ -346,11 +346,11 @@ def _value(self) -> str: value = '' return value - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" if valuelist: # We received a timestamp from the browser. Parse and save it - data: t.Optional[datetime] = None + data: Optional[datetime] = None # `valuelist` will contain `date` and `time` as two separate values # if the widget is rendered as two parts. If so, parse each at a time # and use it as a default to replace values from the next value. If the @@ -384,7 +384,7 @@ def process_formdata(self, valuelist: t.List[str]) -> None: # If the app wanted a naive datetime, strip the timezone info if self.naive: # XXX: cast required because mypy misses the `not None` test above - data = t.cast(datetime, data).replace(tzinfo=None) + data = cast(datetime, data).replace(tzinfo=None) self.data = data else: self.data = None @@ -398,7 +398,7 @@ def _value(self) -> str: return '\r\n'.join(self.data) return '' - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" if valuelist and valuelist[0]: self.data = ( @@ -411,9 +411,9 @@ def process_formdata(self, valuelist: t.List[str]) -> None: class UserSelectFieldBase: """Select a user.""" - data: t.Optional[t.List[t.Any]] + data: Optional[list[Any]] - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: self.lastuser = kwargs.pop('lastuser', None) if self.lastuser is None: if hasattr(current_app, 'login_manager'): @@ -439,7 +439,7 @@ def iter_choices(self) -> ReturnIterChoices: for user in self.data: yield (user.userid, user.pickername, True, {}) - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" super().process_formdata(valuelist) # type: ignore[misc] userids = valuelist @@ -475,7 +475,7 @@ def process_formdata(self, valuelist: t.List[str]) -> None: class UserSelectField(UserSelectFieldBase, StringField): """Render a user select field that allows one user to be selected.""" - data: t.Optional[t.Any] + data: Optional[Any] multiple = False widget = Select2Widget() widget_autocomplete = True @@ -491,7 +491,7 @@ def iter_choices(self) -> ReturnIterChoices: if self.data: yield (self.data.userid, self.data.pickername, True, {}) - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" super().process_formdata(valuelist) if self.data: @@ -503,7 +503,7 @@ def process_formdata(self, valuelist: t.List[str]) -> None: class UserSelectMultiField(UserSelectFieldBase, StringField): """Render a user select field that allows multiple users to be selected.""" - data = t.List[t.Type] + data = list[type] multiple = True widget = Select2Widget() widget_autocomplete = True @@ -512,15 +512,15 @@ class UserSelectMultiField(UserSelectFieldBase, StringField): class AutocompleteFieldBase: """Autocomplete a field.""" - data: t.Optional[t.Union[str, t.List[str]]] + data: Optional[Union[str, list[str]]] def __init__( self, - *args: t.Any, + *args: Any, autocomplete_endpoint: str, results_key: str = 'results', separator: str = ',', - **kwargs: t.Any, + **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.autocomplete_endpoint = autocomplete_endpoint @@ -534,7 +534,7 @@ def iter_choices(self) -> ReturnIterChoices: for user in self.data: yield (str(user), str(user), True, {}) - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" super().process_formdata(valuelist) # type: ignore[misc] # Convert strings into Tag objects @@ -552,7 +552,7 @@ class AutocompleteField(AutocompleteFieldBase, StringField): Does not validate choices server-side. """ - data: t.Optional[str] + data: Optional[str] multiple = False widget = Select2Widget() widget_autocomplete = True @@ -562,7 +562,7 @@ def _value(self) -> str: return self.data return '' - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" super().process_formdata(valuelist) if self.data: @@ -578,7 +578,7 @@ class AutocompleteMultipleField(AutocompleteFieldBase, StringField): Does not validate choices server-side. """ - data: t.Optional[t.List[str]] + data: Optional[list[str]] multiple = True widget = Select2Widget() widget_autocomplete = True @@ -587,11 +587,9 @@ class AutocompleteMultipleField(AutocompleteFieldBase, StringField): class GeonameSelectFieldBase: """Select a geoname location.""" - data: t.Optional[ - t.Union[str, t.List[str], GeonameidProtocol, t.List[GeonameidProtocol]] - ] + data: Optional[Union[str, list[str], GeonameidProtocol, list[GeonameidProtocol]]] - def __init__(self, *args: t.Any, separator: str = ',', **kwargs: t.Any) -> None: + def __init__(self, *args: Any, separator: str = ',', **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.separator = separator server = current_app.config.get('HASCORE_SERVER', 'https://hasgeek.com/api') @@ -607,7 +605,7 @@ def iter_choices(self) -> ReturnIterChoices: for item in self.data: yield (str(item), str(item), True, {}) - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" super().process_formdata(valuelist) # type: ignore[misc] # TODO: Convert strings into GeoName objects @@ -617,7 +615,7 @@ def process_formdata(self, valuelist: t.List[str]) -> None: class GeonameSelectField(GeonameSelectFieldBase, StringField): """Render a geoname select field that allows one geoname to be selected.""" - data: t.Optional[t.Union[str, GeonameidProtocol]] + data: Optional[Union[str, GeonameidProtocol]] multiple = False widget = Select2Widget() widget_autocomplete = True @@ -629,7 +627,7 @@ def _value(self) -> str: return self.data return '' - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" super().process_formdata(valuelist) if self.data: @@ -642,7 +640,7 @@ def process_formdata(self, valuelist: t.List[str]) -> None: class GeonameSelectMultiField(GeonameSelectFieldBase, StringField): """Render a geoname select field that allows multiple geonames to be selected.""" - data: t.Optional[t.Union[t.List[str], t.List[GeonameidProtocol]]] + data: Optional[Union[list[str], list[GeonameidProtocol]]] multiple = True widget = Select2Widget() widget_autocomplete = True @@ -653,10 +651,10 @@ class AnnotatedTextField(StringField): def __init__( self, - *args: t.Any, - prefix: t.Optional[str] = None, - suffix: t.Optional[str] = None, - **kwargs: t.Any, + *args: Any, + prefix: Optional[str] = None, + suffix: Optional[str] = None, + **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.prefix = prefix @@ -666,7 +664,7 @@ def __init__( class MarkdownField(TextAreaField): """TextArea field which has class='markdown'.""" - def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: + def __call__(self, *args: Any, **kwargs: Any) -> str: c = kwargs.pop('class', '') or kwargs.pop('class_', '') kwargs['class'] = (c + ' markdown').strip() return super().__call__(*args, **kwargs) @@ -675,7 +673,7 @@ def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: class StylesheetField(wtforms.TextAreaField): """TextArea field which has class='stylesheet'.""" - def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: + def __call__(self, *args: Any, **kwargs: Any) -> str: c = kwargs.pop('class', '') or kwargs.pop('class_', '') kwargs['class'] = (c + ' stylesheet').strip() return super().__call__(*args, **kwargs) @@ -701,18 +699,18 @@ class ImgeeField(URLField): def __init__( self, - *args: t.Any, - profile: t.Optional[t.Union[str, t.Callable[[], str]]] = None, - img_label: t.Optional[str] = None, - img_size: t.Optional[str] = None, - **kwargs: t.Any, + *args: Any, + profile: Optional[Union[str, Callable[[], str]]] = None, + img_label: Optional[str] = None, + img_size: Optional[str] = None, + **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.profile = profile self.img_label = img_label self.img_size = img_size - def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: + def __call__(self, *args: Any, **kwargs: Any) -> str: c = kwargs.pop('class', '') or kwargs.pop('class_', '') kwargs['class'] = (c + ' imgee__url-holder').strip() if self.profile: @@ -729,7 +727,7 @@ def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: class FormField(wtforms.FormField): """FormField that removes CSRF in sub-forms.""" - def process(self, *args: t.Any, **kwargs: t.Any) -> None: + def process(self, *args: Any, **kwargs: Any) -> None: super().process(*args, **kwargs) if hasattr(self.form, 'csrf_token'): del self.form.csrf_token @@ -738,13 +736,13 @@ def process(self, *args: t.Any, **kwargs: t.Any) -> None: class CoordinatesField(wtforms.Field): """Adds latitude and longitude fields and returns them as a tuple.""" - data: t.Optional[t.Tuple[t.Optional[Decimal], t.Optional[Decimal]]] + data: Optional[tuple[Optional[Decimal], Optional[Decimal]]] widget = CoordinatesInput() - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" - latitude: t.Optional[Decimal] - longitude: t.Optional[Decimal] + latitude: Optional[Decimal] + longitude: Optional[Decimal] if valuelist and len(valuelist) == 2: try: @@ -760,7 +758,7 @@ def process_formdata(self, valuelist: t.List[str]) -> None: else: self.data = None, None - def _value(self) -> t.Tuple[str, str]: + def _value(self) -> tuple[str, str]: if self.data is not None and self.data != (None, None): return str(self.data[0]), str(self.data[1]) return '', '' @@ -773,16 +771,16 @@ class RadioMatrixField(wtforms.Field): Saves each row as either an attr or a dict key on the target field in the object. """ - data: t.Dict[str, t.Any] + data: dict[str, Any] widget = RadioMatrixInput() def __init__( self, - *args: t.Any, - coerce: t.Callable[[t.Any], t.Any] = str, - fields: t.Iterable[t.Tuple[str, str]] = (), - choices: t.Iterable[t.Tuple[str, str]] = (), - **kwargs: t.Any, + *args: Any, + coerce: Callable[[Any], Any] = str, + fields: Iterable[tuple[str, str]] = (), + choices: Iterable[tuple[str, str]] = (), + **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.coerce = coerce @@ -792,8 +790,8 @@ def __init__( def process( self, formdata: MultiDict, - data: t.Any = unset_value, - extra_filters: t.Optional[t.Iterable[t.Callable[[t.Any], t.Any]]] = None, + data: Any = unset_value, + extra_filters: Optional[Iterable[Callable[[Any], Any]]] = None, ) -> None: self.process_errors = [] if data is unset_value: @@ -820,18 +818,18 @@ def process( except ValueError as exc: self.process_errors.append(exc.args[0]) - def process_data(self, value: t.Any) -> None: + def process_data(self, value: Any) -> None: """Process incoming data from Python.""" if value: self.data = {fname: getattr(value, fname) for fname, _ftitle in self.fields} else: self.data = {} - def process_formdata(self, valuelist: t.Dict[str, t.Any]) -> None: + def process_formdata(self, valuelist: dict[str, Any]) -> None: """Process incoming data from request form.""" self.data = {key: self.coerce(value) for key, value in valuelist.items()} - def populate_obj(self, obj: t.Any, name: str) -> None: + def populate_obj(self, obj: Any, name: str) -> None: # 'name' is the name of this field in the form. Ignore it for RadioMatrixField for fname, _ftitle in self.fields: @@ -859,7 +857,7 @@ class MyForm(forms.Form): widget = OriginalSelectWidget() - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: self.lenum = kwargs.pop('lenum') kwargs['choices'] = self.lenum.nametitles() @@ -871,7 +869,7 @@ def iter_choices(self) -> ReturnIterChoices: for name, title in self.choices: yield (name, title, name == selected_name, {}) - def process_data(self, value: t.Any) -> None: + def process_data(self, value: Any) -> None: """Process incoming data from Python.""" if value is None: self.data = None @@ -880,7 +878,7 @@ def process_data(self, value: t.Any) -> None: else: raise KeyError(_("Value not in LabeledEnum")) - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" if valuelist: try: @@ -915,16 +913,16 @@ class MyForm(forms.Form): :param decode_kwargs: Additional arguments for :meth:`json.loads` (default ``{}``) """ - default_encode_kwargs: t.Dict[str, t.Any] = {'sort_keys': True, 'indent': 2} - default_decode_kwargs: t.Dict[str, t.Any] = {} + default_encode_kwargs: dict[str, Any] = {'sort_keys': True, 'indent': 2} + default_decode_kwargs: dict[str, Any] = {} def __init__( self, - *args: t.Any, + *args: Any, require_dict: bool = True, - encode_kwargs: t.Optional[t.Dict[str, t.Any]] = None, - decode_kwargs: t.Optional[t.Dict[str, t.Any]] = None, - **kwargs: t.Any, + encode_kwargs: Optional[dict[str, Any]] = None, + decode_kwargs: Optional[dict[str, Any]] = None, + **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.require_dict = require_dict @@ -935,7 +933,7 @@ def __init__( decode_kwargs if decode_kwargs is not None else self.default_decode_kwargs ) - def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: + def __call__(self, *args: Any, **kwargs: Any) -> str: c = kwargs.pop('class', '') or kwargs.pop('class_', '') kwargs['class'] = (c + ' json').strip() return super().__call__(*args, **kwargs) @@ -956,7 +954,7 @@ def _value(self) -> str: return json.dumps(self.data, ensure_ascii=False, **self.encode_args) return '' - def process_data(self, value: t.Any) -> None: + def process_data(self, value: Any) -> None: """Process incoming data from Python.""" if value is not None and self.require_dict and not isinstance(value, Mapping): raise ValueError(_("Field value must be a dictionary")) @@ -968,7 +966,7 @@ def process_data(self, value: t.Any) -> None: self.data = value - def process_formdata(self, valuelist: t.List[str]) -> None: + def process_formdata(self, valuelist: list[str]) -> None: """Process incoming data from request form.""" if valuelist: value = valuelist[0] diff --git a/src/baseframe/forms/filters.py b/src/baseframe/forms/filters.py index 71367b98..60f93e18 100644 --- a/src/baseframe/forms/filters.py +++ b/src/baseframe/forms/filters.py @@ -18,26 +18,27 @@ value is returned if it's falsy. """ -import typing as t +from collections.abc import Iterable +from typing import Any, Callable, Optional from coaster.utils import unicode_extended_whitespace __all__ = ['lower', 'upper', 'strip', 'lstrip', 'rstrip', 'strip_each', 'none_if_empty'] -def lower() -> t.Callable[[t.Optional[str]], t.Optional[str]]: +def lower() -> Callable[[Optional[str]], Optional[str]]: """Convert data to lower case.""" - def lower_inner(value: t.Optional[str]) -> t.Optional[str]: + def lower_inner(value: Optional[str]) -> Optional[str]: return value.lower() if value else value return lower_inner -def upper() -> t.Callable[[t.Optional[str]], t.Optional[str]]: +def upper() -> Callable[[Optional[str]], Optional[str]]: """Convert data to upper case.""" - def upper_inner(value: t.Optional[str]) -> t.Optional[str]: + def upper_inner(value: Optional[str]) -> Optional[str]: return value.upper() if value else value return upper_inner @@ -45,14 +46,14 @@ def upper_inner(value: t.Optional[str]) -> t.Optional[str]: def strip( chars: str = unicode_extended_whitespace, -) -> t.Callable[[t.Optional[str]], t.Optional[str]]: +) -> Callable[[Optional[str]], Optional[str]]: """ Strip whitespace from both ends. :param chars: If specified, strip these characters instead of whitespace """ - def strip_inner(value: t.Optional[str]) -> t.Optional[str]: + def strip_inner(value: Optional[str]) -> Optional[str]: return value.strip(chars) if value else value return strip_inner @@ -60,14 +61,14 @@ def strip_inner(value: t.Optional[str]) -> t.Optional[str]: def lstrip( chars: str = unicode_extended_whitespace, -) -> t.Callable[[t.Optional[str]], t.Optional[str]]: +) -> Callable[[Optional[str]], Optional[str]]: """ Strip whitespace from beginning of data. :param chars: If specified, strip these characters instead of whitespace """ - def lstrip_inner(value: t.Optional[str]) -> t.Optional[str]: + def lstrip_inner(value: Optional[str]) -> Optional[str]: return value.lstrip(chars) if value else value return lstrip_inner @@ -75,14 +76,14 @@ def lstrip_inner(value: t.Optional[str]) -> t.Optional[str]: def rstrip( chars: str = unicode_extended_whitespace, -) -> t.Callable[[t.Optional[str]], t.Optional[str]]: +) -> Callable[[Optional[str]], Optional[str]]: """ Strip whitespace from end of data. :param chars: If specified, strip these characters instead of whitespace """ - def rstrip_inner(value: t.Optional[str]) -> t.Optional[str]: + def rstrip_inner(value: Optional[str]) -> Optional[str]: return value.rstrip(chars) if value else value return rstrip_inner @@ -90,7 +91,7 @@ def rstrip_inner(value: t.Optional[str]) -> t.Optional[str]: def strip_each( chars: str = unicode_extended_whitespace, -) -> t.Callable[[t.Optional[t.Iterable[str]]], t.Optional[t.Iterable[str]]]: +) -> Callable[[Optional[Iterable[str]]], Optional[Iterable[str]]]: """ Strip whitespace and remove blank elements from each element in an iterable. @@ -100,8 +101,8 @@ def strip_each( """ def strip_each_inner( - value: t.Optional[t.Iterable[str]], - ) -> t.Optional[t.Iterable[str]]: + value: Optional[Iterable[str]], + ) -> Optional[Iterable[str]]: if value: return [sline for sline in [line.strip(chars) for line in value] if sline] return value @@ -109,10 +110,10 @@ def strip_each_inner( return strip_each_inner -def none_if_empty() -> t.Callable[[t.Any], t.Optional[t.Any]]: +def none_if_empty() -> Callable[[Any], Optional[Any]]: """If data is empty or evaluates to boolean false, replace with None.""" - def none_if_empty_inner(value: t.Any) -> t.Optional[t.Any]: + def none_if_empty_inner(value: Any) -> Optional[Any]: return value if value else None return none_if_empty_inner diff --git a/src/baseframe/forms/form.py b/src/baseframe/forms/form.py index 20b1e5e1..b65cc94a 100644 --- a/src/baseframe/forms/form.py +++ b/src/baseframe/forms/form.py @@ -2,9 +2,10 @@ from __future__ import annotations -import typing as t -import typing_extensions as te import warnings +from collections.abc import Iterable +from typing import Any, Callable, Optional, Union +from typing_extensions import TypeAlias import wtforms from flask import current_app @@ -32,7 +33,7 @@ ] # Use a hardcoded list to control what is available to user-facing apps -field_registry: t.Dict[str, WTField] = { +field_registry: dict[str, WTField] = { 'SelectField': bparsleyjs.SelectField, 'SelectMultipleField': bfields.SelectMultipleField, 'RadioField': bparsleyjs.RadioField, @@ -60,16 +61,16 @@ 'ImageField': bfields.ImgeeField, } -WidgetRegistryEntry: te.TypeAlias = t.Tuple[t.Callable[..., WidgetProtocol]] -widget_registry: t.Dict[str, WidgetRegistryEntry] = {} +WidgetRegistryEntry: TypeAlias = tuple[Callable[..., WidgetProtocol]] +widget_registry: dict[str, WidgetRegistryEntry] = {} -ValidatorRegistryEntry: te.TypeAlias = t.Union[ - t.Tuple[t.Callable[..., ValidatorCallable]], - t.Tuple[t.Callable[..., ValidatorCallable], str], - t.Tuple[t.Callable[..., ValidatorCallable], str, str], - t.Tuple[t.Callable[..., ValidatorCallable], str, str, str], +ValidatorRegistryEntry: TypeAlias = Union[ + tuple[Callable[..., ValidatorCallable]], + tuple[Callable[..., ValidatorCallable], str], + tuple[Callable[..., ValidatorCallable], str, str], + tuple[Callable[..., ValidatorCallable], str, str, str], ] -validator_registry: t.Dict[str, ValidatorRegistryEntry] = { +validator_registry: dict[str, ValidatorRegistryEntry] = { 'Length': (wtforms.validators.Length, 'min', 'max', 'message'), 'NumberRange': (wtforms.validators.NumberRange, 'min', 'max', 'message'), 'Optional': (wtforms.validators.Optional, 'strip_whitespace'), @@ -81,11 +82,11 @@ 'AllUrlsValid': (bvalidators.AllUrlsValid,), } -FilterRegistryEntry: te.TypeAlias = t.Union[ - t.Tuple[t.Callable[..., FilterCallable]], - t.Tuple[t.Callable[..., FilterCallable], str], +FilterRegistryEntry: TypeAlias = Union[ + tuple[Callable[..., FilterCallable]], + tuple[Callable[..., FilterCallable], str], ] -filter_registry: t.Dict[str, FilterRegistryEntry] = { +filter_registry: dict[str, FilterRegistryEntry] = { 'lower': (bfilters.lower,), 'upper': (bfilters.upper,), 'strip': (bfilters.strip, 'chars'), @@ -98,10 +99,10 @@ class Form(BaseForm): """Form with additional methods.""" - __expects__: t.Iterable[str] = () - __returns__: t.Iterable[str] = () + __expects__: Iterable[str] = () + __returns__: Iterable[str] = () - def __init_subclass__(cls, **kwargs: t.Any) -> None: + def __init_subclass__(cls, **kwargs: Any) -> None: """Validate :attr:`__expects__` and :attr:`__returns__` in sub-classes.""" super().__init_subclass__(**kwargs) if {'edit_obj', 'edit_model', 'edit_parent', 'edit_id'} & set(cls.__expects__): @@ -121,7 +122,7 @@ def __init_subclass__(cls, **kwargs: t.Any) -> None: stacklevel=2, ) - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: for attr in self.__expects__: if attr not in kwargs: raise TypeError(f"Expected parameter {attr} was not supplied") @@ -155,11 +156,11 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: elif callable(post_init := getattr(self, 'set_queries', None)): post_init() # pylint: disable=not-callable - def __json__(self) -> t.List[t.Any]: + def __json__(self) -> Any: """Render this form as JSON.""" return [field.__json__() for field in self._fields.values()] - def populate_obj(self, obj: t.Any) -> None: + def populate_obj(self, obj: Any) -> None: """ Populate the attributes of the passed `obj` with data from the form's fields. @@ -178,13 +179,11 @@ def populate_obj(self, obj: t.Any) -> None: def process( self, - formdata: t.Optional[MultiDict] = None, - obj: t.Any = None, - data: t.Optional[t.Dict[str, t.Any]] = None, - extra_filters: t.Optional[ - t.Dict[str, t.Iterable[t.Callable[[t.Any], t.Any]]] - ] = None, - **kwargs: t.Any, + formdata: Optional[MultiDict] = None, + obj: Any = None, + data: Optional[dict[str, Any]] = None, + extra_filters: Optional[dict[str, Iterable[Callable[[Any], Any]]]] = None, + **kwargs: Any, ) -> None: """ Take form, object data, and keyword arg input and have the fields process them. @@ -236,7 +235,7 @@ def process( def validate( self, - extra_validators: t.Optional[t.Dict[str, ValidatorList]] = None, + extra_validators: Optional[dict[str, ValidatorList]] = None, send_signals: bool = True, ) -> bool: """ @@ -258,7 +257,7 @@ def validate( self.send_signals(success) return success - def send_signals(self, success: t.Optional[bool] = None) -> None: + def send_signals(self, success: Optional[bool] = None) -> None: if success is None: success = not self.errors if success: @@ -286,10 +285,10 @@ class FormGenerator: def __init__( self, - fields: t.Optional[t.Dict[str, WTField]] = None, - widgets: t.Optional[t.Dict[str, WidgetRegistryEntry]] = None, - validators: t.Optional[t.Dict[str, ValidatorRegistryEntry]] = None, - filters: t.Optional[t.Dict[str, FilterRegistryEntry]] = None, + fields: Optional[dict[str, WTField]] = None, + widgets: Optional[dict[str, WidgetRegistryEntry]] = None, + validators: Optional[dict[str, ValidatorRegistryEntry]] = None, + filters: Optional[dict[str, FilterRegistryEntry]] = None, default_field: str = 'StringField', ) -> None: # If using global defaults, make a copy in this class so that @@ -302,7 +301,7 @@ def __init__( self.default_field = default_field # TODO: Make `formstruct` a TypedDict - def generate(self, formstruct: dict) -> t.Type[Form]: + def generate(self, formstruct: dict) -> type[Form]: """Generate a dynamic form from the given data structure.""" class DynamicForm(Form): @@ -366,7 +365,7 @@ class RecaptchaForm(Form): recaptcha = bfields.RecaptchaField() - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) if not ( current_app.config.get('RECAPTCHA_PUBLIC_KEY') diff --git a/src/baseframe/forms/parsleyjs.py b/src/baseframe/forms/parsleyjs.py index e8779d0d..6fbd3398 100644 --- a/src/baseframe/forms/parsleyjs.py +++ b/src/baseframe/forms/parsleyjs.py @@ -14,7 +14,7 @@ import copy import re -import typing as t +from typing import Any, Union from markupsafe import Markup from wtforms import Field as WTField @@ -100,9 +100,7 @@ ] -def parsley_kwargs( - field: WTField, kwargs: t.Any, extend: bool = True -) -> t.Dict[str, t.Any]: +def parsley_kwargs(field: WTField, kwargs: Any, extend: bool = True) -> dict[str, Any]: """ Generate updated kwargs from the validators present for the widget. @@ -116,7 +114,7 @@ def parsley_kwargs( one. Do check if the behaviour suits your needs. """ if extend: - new_kwargs: t.Dict[str, t.Any] = copy.deepcopy(kwargs) + new_kwargs: dict[str, Any] = copy.deepcopy(kwargs) else: new_kwargs = {} for vali in field.validators: @@ -145,15 +143,15 @@ def parsley_kwargs( return new_kwargs -def _email_kwargs(kwargs: t.Dict[str, t.Any], vali: ValidatorCallable) -> None: +def _email_kwargs(kwargs: dict[str, Any], vali: ValidatorCallable) -> None: kwargs['data-parsley-type'] = 'email' -def _equal_to_kwargs(kwargs: t.Dict[str, t.Any], vali: EqualTo) -> None: +def _equal_to_kwargs(kwargs: dict[str, Any], vali: EqualTo) -> None: kwargs['data-parsley-equalto'] = '#' + vali.fieldname -def _ip_address_kwargs(kwargs: t.Dict[str, t.Any], vali: IPAddress) -> None: +def _ip_address_kwargs(kwargs: dict[str, Any], vali: IPAddress) -> None: # Regexp from http://stackoverflow.com/a/4460645 kwargs['data-parsley-regexp'] = ( r'^\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' @@ -163,7 +161,7 @@ def _ip_address_kwargs(kwargs: t.Dict[str, t.Any], vali: IPAddress) -> None: ) -def _length_kwargs(kwargs: t.Dict[str, t.Any], vali: Length) -> None: +def _length_kwargs(kwargs: dict[str, Any], vali: Length) -> None: default_number = -1 if default_number not in (vali.min, vali.max): @@ -179,19 +177,19 @@ def _length_kwargs(kwargs: t.Dict[str, t.Any], vali: Length) -> None: kwargs['data-parsley-maxlength'] = str(vali.max) -def _number_range_kwargs(kwargs: t.Dict[str, t.Any], vali: NumberRange) -> None: +def _number_range_kwargs(kwargs: dict[str, Any], vali: NumberRange) -> None: kwargs['data-parsley-range'] = '[' + str(vali.min) + ',' + str(vali.max) + ']' def _input_required_kwargs( - kwargs: t.Dict[str, t.Any], vali: t.Union[InputRequired, DataRequired] + kwargs: dict[str, Any], vali: Union[InputRequired, DataRequired] ) -> None: kwargs['data-parsley-required'] = 'true' if vali.message: kwargs['data-parsley-required-message'] = vali.message -def _regexp_kwargs(kwargs: t.Dict[str, t.Any], vali: Regexp) -> None: +def _regexp_kwargs(kwargs: dict[str, Any], vali: Regexp) -> None: if isinstance(vali.regex, re.Pattern): # WTForms allows compiled regexps to be passed to the validator, but we need # the pattern text @@ -201,11 +199,11 @@ def _regexp_kwargs(kwargs: t.Dict[str, t.Any], vali: Regexp) -> None: kwargs['data-parsley-regexp'] = regex_string -def _url_kwargs(kwargs: t.Dict[str, t.Any], vali: URL) -> None: +def _url_kwargs(kwargs: dict[str, Any], vali: URL) -> None: kwargs['data-parsley-type'] = 'url' -def _string_seq_delimiter(kwargs: t.Dict[str, t.Any], vali: AnyOf) -> str: +def _string_seq_delimiter(kwargs: dict[str, Any], vali: AnyOf) -> str: # We normally use a comma as the delimiter - looks clean and it's parsley's default. # If the strings for which we check contain a comma, we cannot use it as a # delimiter. @@ -221,23 +219,21 @@ def _string_seq_delimiter(kwargs: t.Dict[str, t.Any], vali: AnyOf) -> str: return delimiter -def _anyof_kwargs(kwargs: t.Dict[str, t.Any], vali: AnyOf) -> None: +def _anyof_kwargs(kwargs: dict[str, Any], vali: AnyOf) -> None: delimiter = _string_seq_delimiter(kwargs, vali) kwargs['data-parsley-inlist'] = delimiter.join(vali.values) -def _trigger_kwargs( - kwargs: t.Dict[str, t.Any], trigger: str = 'change focusout' -) -> None: +def _trigger_kwargs(kwargs: dict[str, Any], trigger: str = 'change focusout') -> None: kwargs['data-parsley-trigger'] = trigger -def _message_kwargs(kwargs: t.Dict[str, t.Any], message: str) -> None: +def _message_kwargs(kwargs: dict[str, Any], message: str) -> None: kwargs['data-parsley-error-message'] = message class ParsleyInputMixin: - def __call__(self, field: WTField, **kwargs: t.Any) -> str: + def __call__(self, field: WTField, **kwargs: Any) -> str: kwargs = parsley_kwargs(field, kwargs) return super().__call__(field, **kwargs) # type: ignore[misc] @@ -287,7 +283,7 @@ class Select(ParsleyInputMixin, _Select): class ListWidget(_ListWidget): - def __call__(self, field: WTField, **kwargs: t.Any) -> str: + def __call__(self, field: WTField, **kwargs: Any) -> str: sub_kwargs = parsley_kwargs(field, kwargs, extend=False) kwargs.setdefault('id', field.id) html = [f'<{self.html_tag} {html_params(**kwargs)}>'] diff --git a/src/baseframe/forms/patch_wtforms.py b/src/baseframe/forms/patch_wtforms.py index 8cf7a89b..a2e5c29f 100644 --- a/src/baseframe/forms/patch_wtforms.py +++ b/src/baseframe/forms/patch_wtforms.py @@ -1,6 +1,6 @@ """Patches WTForms to add additional functionality as required by Baseframe.""" -import typing as t +from typing import Any import wtforms @@ -8,7 +8,7 @@ def _patch_wtforms_add_flags() -> None: - def add_flags(validator: ValidatorProtocol, flags: t.Dict[str, t.Any]) -> None: + def add_flags(validator: ValidatorProtocol, flags: dict[str, Any]) -> None: validator_flags = dict(getattr(validator, 'field_flags', {})) # Make a copy validator_flags.update(flags) # Add new flags validator.field_flags = validator_flags # Add back into validator @@ -23,7 +23,7 @@ def add_flags(validator: ValidatorProtocol, flags: t.Dict[str, t.Any]) -> None: def _patch_json_output() -> None: """Add __json__ method to Field class.""" - def field_json(self: wtforms.Field) -> t.Dict[str, t.Any]: + def field_json(self: wtforms.Field) -> Any: """Render field to a JSON-compatible dictionary.""" return { 'name': self.name, diff --git a/src/baseframe/forms/sqlalchemy.py b/src/baseframe/forms/sqlalchemy.py index 5c44a4dc..d40fde0b 100644 --- a/src/baseframe/forms/sqlalchemy.py +++ b/src/baseframe/forms/sqlalchemy.py @@ -1,6 +1,6 @@ """SQLAlchemy-based form fields and widgets.""" -import typing as t +from typing import Any, Optional from wtforms import Field as WTField, Form as WTForm from wtforms_sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField @@ -20,7 +20,7 @@ class AvailableAttr: """Check whether the specified attribute is available for the model being edited.""" def __init__( - self, attr: str, message: t.Optional[str] = None, model: t.Any = None + self, attr: str, message: Optional[str] = None, model: Any = None ) -> None: self.model = model self.attr = attr @@ -48,7 +48,7 @@ def __call__(self, form: WTForm, field: WTField) -> None: class AvailableName(AvailableAttr): """Check whether the specified name is available for the model being edited.""" - def __init__(self, message: t.Optional[str] = None, model: t.Any = None) -> None: + def __init__(self, message: Optional[str] = None, model: Any = None) -> None: if not message: message = __("This URL name is already in use") super().__init__('name', message, model) diff --git a/src/baseframe/forms/typing.py b/src/baseframe/forms/typing.py index 4b5231f1..bd0dc1ba 100644 --- a/src/baseframe/forms/typing.py +++ b/src/baseframe/forms/typing.py @@ -1,33 +1,34 @@ """Form type aliases and protocols.""" -import typing as t -import typing_extensions as te +from collections.abc import Generator, Iterable, Sequence +from typing import Any, Callable +from typing_extensions import Protocol, TypeAlias from markupsafe import Markup from wtforms import Field as WTField, Form as WTForm -FilterCallable: te.TypeAlias = t.Callable[[t.Any], t.Any] -FilterList: te.TypeAlias = t.Iterable[FilterCallable] -ReturnIterChoices: te.TypeAlias = t.Generator[ - t.Tuple[str, str, bool, t.Dict[str, t.Any]], None, None +FilterCallable: TypeAlias = Callable[[Any], Any] +FilterList: TypeAlias = Iterable[FilterCallable] +ReturnIterChoices: TypeAlias = Generator[ + tuple[str, str, bool, dict[str, Any]], None, None ] -ValidatorCallable: te.TypeAlias = t.Callable[[WTForm, WTField], None] -ValidatorList: te.TypeAlias = t.Sequence[ValidatorCallable] -ValidatorConstructor: te.TypeAlias = t.Callable[..., ValidatorCallable] +ValidatorCallable: TypeAlias = Callable[[WTForm, WTField], None] +ValidatorList: TypeAlias = Sequence[ValidatorCallable] +ValidatorConstructor: TypeAlias = Callable[..., ValidatorCallable] -class WidgetProtocol(te.Protocol): +class WidgetProtocol(Protocol): """Protocol for a WTForms widget.""" - def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: ... + def __call__(self, field: WTField, **kwargs: Any) -> Markup: ... -WidgetConstructor: te.TypeAlias = t.Callable[..., WidgetProtocol] +WidgetConstructor: TypeAlias = Callable[..., WidgetProtocol] -class ValidatorProtocol(te.Protocol): +class ValidatorProtocol(Protocol): """Protocol for validators.""" - field_flags: t.Dict[str, bool] + field_flags: dict[str, bool] def __call__(self, form: WTForm, field: WTField) -> None: ... diff --git a/src/baseframe/forms/validators.py b/src/baseframe/forms/validators.py index 490dda5d..fc38712f 100644 --- a/src/baseframe/forms/validators.py +++ b/src/baseframe/forms/validators.py @@ -4,11 +4,11 @@ import datetime import re -import typing as t from collections import namedtuple +from collections.abc import Iterable from decimal import Decimal from fractions import Fraction -from typing import Any, cast +from typing import Any, Callable, Optional as OptionalType, Union, cast from urllib.parse import urljoin, urlparse import dns.resolver @@ -79,14 +79,14 @@ 'invalid-input-response': __("The response parameter is invalid or malformed"), } -InvalidUrlPatterns = t.Iterable[t.Tuple[t.Iterable[t.Any], str]] -AllowedListInit = t.Optional[ - t.Union[t.Iterable[str], t.Callable[[], t.Optional[t.Iterable[str]]]] +InvalidUrlPatterns = Iterable[tuple[Iterable[Any], str]] +AllowedListInit = OptionalType[ + Union[Iterable[str], Callable[[], OptionalType[Iterable[str]]]] ] -AllowedList = t.Optional[t.Iterable[str]] +AllowedList = OptionalType[Iterable[str]] -def is_empty(value: t.Any) -> bool: +def is_empty(value: Any) -> bool: """Return True if the value is falsy but not a numeric zero.""" return value not in _zero_values and not value @@ -130,7 +130,7 @@ class AllowedIf: default_message = __("This requires ‘{field}’ to be specified") - def __init__(self, fieldname: str, message: t.Optional[str] = None) -> None: + def __init__(self, fieldname: str, message: OptionalType[str] = None) -> None: self.fieldname = fieldname self.message = message or self.default_message @@ -161,7 +161,7 @@ class OptionalIf(Optional): default_message = __("This is required") - def __init__(self, fieldname: str, message: t.Optional[str] = None) -> None: + def __init__(self, fieldname: str, message: OptionalType[str] = None) -> None: super().__init__() self.fieldname = fieldname self.message = message or self.default_message @@ -190,7 +190,7 @@ class RequiredIf(DataRequired): default_message = __("This is required") - def __init__(self, fieldname: str, message: t.Optional[str] = None) -> None: + def __init__(self, fieldname: str, message: OptionalType[str] = None) -> None: message = message or self.default_message super().__init__(message=message) self.fieldname = fieldname @@ -206,7 +206,7 @@ class _Comparison: default_message = __("Comparison failed") - def __init__(self, fieldname: str, message: t.Optional[str] = None) -> None: + def __init__(self, fieldname: str, message: OptionalType[str] = None) -> None: self.fieldname = fieldname self.message = message or self.default_message @@ -221,7 +221,7 @@ def __call__(self, form: WTForm, field: WTField) -> None: } raise ValidationError(self.message.format(**d)) - def compare(self, value: t.Any, other: t.Any) -> bool: + def compare(self, value: Any, other: Any) -> bool: raise NotImplementedError("Subclasses must define ``compare``") @@ -239,7 +239,7 @@ class GreaterThan(_Comparison): default_message = __("This must be greater than {other_label}") - def compare(self, value: t.Any, other: t.Any) -> bool: + def compare(self, value: Any, other: Any) -> bool: return value > other @@ -257,7 +257,7 @@ class GreaterThanEqualTo(_Comparison): default_message = __("This must be greater than or equal to {other_label}") - def compare(self, value: t.Any, other: t.Any) -> bool: + def compare(self, value: Any, other: Any) -> bool: return value >= other @@ -275,7 +275,7 @@ class LesserThan(_Comparison): default_message = __("This must be lesser than {other_label}") - def compare(self, value: t.Any, other: t.Any) -> bool: + def compare(self, value: Any, other: Any) -> bool: return value < other @@ -293,7 +293,7 @@ class LesserThanEqualTo(_Comparison): default_message = __("This must be lesser than or equal to {other_label}") - def compare(self, value: t.Any, other: t.Any) -> bool: + def compare(self, value: Any, other: Any) -> bool: return value <= other @@ -311,7 +311,7 @@ class NotEqualTo(_Comparison): default_message = __("This must not be the same as {other_label}") - def compare(self, value: t.Any, other: t.Any) -> bool: + def compare(self, value: Any, other: Any) -> bool: return value != other @@ -325,7 +325,7 @@ class IsEmoji: default_message = __("This is not a valid emoji") - def __init__(self, message: t.Optional[str] = None) -> None: + def __init__(self, message: OptionalType[str] = None) -> None: self.message = message or self.default_message def __call__(self, form: WTForm, field: WTField) -> None: @@ -346,7 +346,7 @@ class IsPublicEmailDomain: default_message = __("This domain is not a public email domain") - def __init__(self, message: t.Optional[str] = None, timeout: int = 30) -> None: + def __init__(self, message: OptionalType[str] = None, timeout: int = 30) -> None: self.message = message or self.default_message self.timeout = timeout @@ -369,7 +369,7 @@ class IsNotPublicEmailDomain: default_message = __("This domain is a public email domain") - def __init__(self, message: t.Optional[str] = None, timeout: int = 30) -> None: + def __init__(self, message: OptionalType[str] = None, timeout: int = 30) -> None: self.message = message or self.default_message self.timeout = timeout @@ -390,7 +390,7 @@ class ValidEmail: default_message = __("This email address does not appear to be valid") - def __init__(self, message: t.Optional[str] = None) -> None: + def __init__(self, message: OptionalType[str] = None) -> None: self.message = message def __call__(self, form: WTForm, field: WTField) -> None: @@ -446,13 +446,13 @@ class ValidUrl: def __init__( self, - message: t.Optional[str] = None, - message_urltext: t.Optional[str] = None, - message_schemes: t.Optional[str] = None, - message_domains: t.Optional[str] = None, + message: OptionalType[str] = None, + message_urltext: OptionalType[str] = None, + message_schemes: OptionalType[str] = None, + message_domains: OptionalType[str] = None, invalid_urls: InvalidUrlPatterns = (), - allowed_schemes: t.Optional[AllowedListInit] = None, - allowed_domains: t.Optional[AllowedListInit] = None, + allowed_schemes: OptionalType[AllowedListInit] = None, + allowed_domains: OptionalType[AllowedListInit] = None, visit_url: bool = True, ) -> None: self.message = message or self.default_message @@ -470,8 +470,8 @@ def check_url( allowed_schemes: AllowedList, allowed_domains: AllowedList, invalid_urls: InvalidUrlPatterns, - text: t.Union[str, None] = None, - ) -> t.Optional[str]: + text: Union[str, None] = None, + ) -> OptionalType[str]: """ Inner method to actually check the URL. @@ -666,7 +666,7 @@ class NoObfuscatedEmail: default_message = __("Email address identified") - def __init__(self, message: t.Optional[str] = None) -> None: + def __init__(self, message: OptionalType[str] = None) -> None: self.message = message or self.default_message def __call__(self, form: WTForm, field: WTField) -> None: @@ -686,7 +686,7 @@ class ValidName: "It should have letters, numbers and non-terminal hyphens only" ) - def __init__(self, message: t.Optional[str] = None) -> None: + def __init__(self, message: OptionalType[str] = None) -> None: self.message = message or self.default_message def __call__(self, form: WTForm, field: WTField) -> None: @@ -701,9 +701,9 @@ class ValidCoordinates: def __init__( self, - message: t.Optional[str] = None, - message_latitude: t.Optional[str] = None, - message_longitude: t.Optional[str] = None, + message: OptionalType[str] = None, + message_latitude: OptionalType[str] = None, + message_longitude: OptionalType[str] = None, ) -> None: self.message = message or self.default_message self.message_latitude = message_latitude or self.default_message_latitude @@ -724,7 +724,9 @@ class Recaptcha: default_message_network = __("The server was temporarily unreachable. Try again") def __init__( - self, message: t.Optional[str] = None, message_network: t.Optional[str] = None + self, + message: OptionalType[str] = None, + message_network: OptionalType[str] = None, ) -> None: if message is None: message = RECAPTCHA_ERROR_CODES['missing-input-response'] diff --git a/src/baseframe/forms/widgets.py b/src/baseframe/forms/widgets.py index 7f8474c8..af59e83e 100644 --- a/src/baseframe/forms/widgets.py +++ b/src/baseframe/forms/widgets.py @@ -2,7 +2,7 @@ from __future__ import annotations -import typing as t +from typing import TYPE_CHECKING, Any import wtforms from flask import current_app, render_template @@ -31,7 +31,7 @@ class SelectWidget(Select): """Add support of choices with ``optgroup`` to the ``Select`` widget.""" - def __call__(self, field: SelectFieldBase, **kwargs: t.Any) -> Markup: + def __call__(self, field: SelectFieldBase, **kwargs: Any) -> Markup: kwargs.setdefault('id', field.id) if self.multiple: kwargs['multiple'] = True @@ -61,7 +61,7 @@ def __call__(self, field: SelectFieldBase, **kwargs: t.Any) -> Markup: class Select2Widget(Select): """Add a select2 class to the rendered select widget.""" - def __call__(self, field: SelectFieldBase, **kwargs: t.Any) -> Markup: + def __call__(self, field: SelectFieldBase, **kwargs: Any) -> Markup: kwargs.setdefault('id', field.id) kwargs.pop('type', field.type) if field.multiple: @@ -85,7 +85,7 @@ class TinyMce4(wtforms.widgets.TextArea): #: Used as an identifier in forms.html.jinja2 input_type: str = 'tinymce4' - def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: c = kwargs.pop('class', '') or kwargs.pop('class_', '') if c: kwargs['class'] = f'richtext {c}' @@ -97,11 +97,11 @@ def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: class SubmitInput(wtforms.widgets.SubmitInput): """Submit input with pre-defined classes.""" - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: self.css_class = kwargs.pop('class', '') or kwargs.pop('class_', '') super().__init__(*args, **kwargs) - def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: c = kwargs.pop('class', '') or kwargs.pop('class_', '') kwargs['class'] = f'{self.css_class} {c}' return super().__call__(field, **kwargs) @@ -112,7 +112,7 @@ class DateTimeInput(wtforms.widgets.Input): input_type = 'datetime-local' - def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: kwargs.setdefault('id', field.id) field_id = kwargs.pop('id') kwargs.pop('type', None) @@ -132,7 +132,7 @@ class CoordinatesInput(wtforms.widgets.core.Input): input_type = 'text' - def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: id_ = kwargs.pop('id', field.id) kwargs.setdefault('type', self.input_type) kwargs.setdefault('size', 10) # 9 digits precision and +/- sign @@ -173,7 +173,7 @@ def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: class RadioMatrixInput: """Render a table with a radio matrix.""" - def __call__(self, field: RadioMatrixField, **kwargs: t.Any) -> Markup: + def __call__(self, field: RadioMatrixField, **kwargs: Any) -> Markup: rendered = [] table_class = kwargs.pop('table_class', 'table') rendered.append(f'
{escape(title)} | ') selected = field.data.get(name) for value, _label in field.choices: - params: t.Dict[str, t.Any] = { + params: dict[str, Any] = { 'type': 'radio', 'name': name, 'value': value, @@ -228,7 +228,7 @@ def __init__( self.class_ = class_ self.class_prefix = class_prefix - def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: kwargs.setdefault('id', field.id) kwargs['class_'] = ( kwargs.pop('class_', kwargs.pop('class', '')).strip() + ' ' + self.class_ @@ -247,7 +247,7 @@ def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: class ImgeeWidget(wtforms.widgets.Input): input_type = 'hidden' - def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: + def __call__(self, field: WTField, **kwargs: Any) -> Markup: id_ = kwargs.pop('id', field.id) kwargs.setdefault('type', self.input_type) imgee_host = current_app.config.get('IMGEE_HOST') @@ -298,5 +298,5 @@ def __call__(self, field: WTField, **kwargs: t.Any) -> Markup: ) -if t.TYPE_CHECKING: +if TYPE_CHECKING: from .fields import RadioMatrixField diff --git a/src/baseframe/statsd.py b/src/baseframe/statsd.py index 2b9d7061..2f9a102c 100644 --- a/src/baseframe/statsd.py +++ b/src/baseframe/statsd.py @@ -1,8 +1,8 @@ """Statsd logger.""" import time -import typing as t from datetime import timedelta +from typing import Optional, Union from flask import ( Flask, @@ -28,7 +28,7 @@ REQUEST_START_TIME_ATTR = 'statsd_request_start_time' TEMPLATE_START_TIME_ATTR = 'statsd_template_start_time' -TagsType = t.Dict[str, t.Union[int, str, None]] +TagsType = dict[str, Union[int, str, None]] class Statsd: @@ -79,7 +79,7 @@ class Statsd: The Datadog and SignalFx tag formats are not supported at this time. """ - def __init__(self, app: t.Optional[Flask] = None) -> None: + def __init__(self, app: Optional[Flask] = None) -> None: if app is not None: self.init_app(app) @@ -109,7 +109,7 @@ def init_app(self, app: Flask) -> None: before_render_template.connect(self._before_render_template, app) template_rendered.connect(self._template_rendered, app) - def _metric_name(self, name: str, tags: t.Optional[TagsType] = None) -> str: + def _metric_name(self, name: str, tags: Optional[TagsType] = None) -> str: if tags is None: tags = {} if current_app.config['STATSD_TAGS']: @@ -131,8 +131,8 @@ def _metric_name(self, name: str, tags: t.Optional[TagsType] = None) -> str: def timer( self, stat: str, - rate: t.Optional[t.Union[int, float]] = None, - tags: t.Optional[TagsType] = None, + rate: Optional[Union[int, float]] = None, + tags: Optional[TagsType] = None, ) -> Timer: """Return a Timer.""" stat = self._metric_name(stat, tags) @@ -143,9 +143,9 @@ def timer( def timing( self, stat: str, - delta: t.Union[int, timedelta], - rate: t.Optional[t.Union[int, float]] = None, - tags: t.Optional[TagsType] = None, + delta: Union[int, timedelta], + rate: Optional[Union[int, float]] = None, + tags: Optional[TagsType] = None, ) -> None: """ Send new timing information. @@ -163,8 +163,8 @@ def incr( self, stat: str, count: int = 1, - rate: t.Optional[t.Union[int, float]] = None, - tags: t.Optional[TagsType] = None, + rate: Optional[Union[int, float]] = None, + tags: Optional[TagsType] = None, ) -> None: """Increment a stat by `count`.""" stat = self._metric_name(stat, tags) @@ -178,8 +178,8 @@ def decr( self, stat: str, count: int = 1, - rate: t.Optional[t.Union[int, float]] = None, - tags: t.Optional[TagsType] = None, + rate: Optional[Union[int, float]] = None, + tags: Optional[TagsType] = None, ) -> None: """Decrement a stat by `count`.""" stat = self._metric_name(stat, tags) @@ -193,9 +193,9 @@ def gauge( self, stat: str, value: int, - rate: t.Optional[t.Union[int, float]] = None, + rate: Optional[Union[int, float]] = None, delta: bool = False, - tags: t.Optional[TagsType] = None, + tags: Optional[TagsType] = None, ) -> None: """Set a gauge value.""" stat = self._metric_name(stat, tags) @@ -210,8 +210,8 @@ def set( # noqa: A003 self, stat: str, value: str, - rate: t.Optional[t.Union[int, float]] = None, - tags: t.Optional[TagsType] = None, + rate: Optional[Union[int, float]] = None, + tags: Optional[TagsType] = None, ) -> None: """Set a set value.""" stat = self._metric_name(stat, tags) @@ -276,7 +276,7 @@ def _template_rendered(self, app: Flask, template: Template, **kwargs) -> None: ) return - metrics: t.List[t.Tuple[str, t.Dict[str, t.Optional[t.Union[int, str]]]]] = [ + metrics: list[tuple[str, dict[str, Optional[Union[int, str]]]]] = [ ( 'render_template', {'template': template.name or '_str'}, diff --git a/src/baseframe/utils.py b/src/baseframe/utils.py index 081ccd88..0a56969a 100644 --- a/src/baseframe/utils.py +++ b/src/baseframe/utils.py @@ -2,9 +2,9 @@ import gettext import types -import typing as t from collections import abc from datetime import datetime, time, tzinfo +from typing import Any, Optional, Union, cast import pycountry from babel import Locale @@ -44,7 +44,7 @@ class JSONProvider(JSONProviderBase): """ @staticmethod - def default(o: t.Any) -> t.Any: + def default(o: Any) -> Any: if isinstance(o, BaseTzInfo): # BaseTzInfo is a subclass of tzinfo, so it must be checked first return o.zone @@ -70,7 +70,7 @@ def request_timestamp() -> datetime: def is_public_email_domain( - email_or_domain: str, default: t.Optional[bool] = None, timeout: int = 30 + email_or_domain: str, default: Optional[bool] = None, timeout: int = 30 ) -> bool: """ Return True if the given email domain is known to offer public email accounts. @@ -86,11 +86,11 @@ def is_public_email_domain( :param timeout: Lookup timeout in seconds :raises MxLookupError: If a DNS lookup error happens and no default is specified """ - sniffedmx: t.Optional[dict] + sniffedmx: Optional[dict] cache_key = 'mxrecord/' + md5sum(email_or_domain) try: - sniffedmx = t.cast(t.Optional[dict], asset_cache.get(cache_key)) + sniffedmx = cast(Optional[dict], asset_cache.get(cache_key)) except ValueError: # Possible error from Py2 vs Py3 pickle mismatch sniffedmx = None @@ -110,7 +110,7 @@ def is_public_email_domain( return False -def localized_country_list() -> t.List[t.Tuple[str, str]]: +def localized_country_list() -> list[tuple[str, str]]: """ Return a list of countries localized to the current user's locale. @@ -121,7 +121,7 @@ def localized_country_list() -> t.List[t.Tuple[str, str]]: @cache.memoize(timeout=86400) -def _localized_country_list_inner(locale: str) -> t.List[t.Tuple[str, str]]: +def _localized_country_list_inner(locale: str) -> list[tuple[str, str]]: """Return localized country list (helper for :func:`localized_country_list`).""" if locale == 'en': countries = [(country.name, country.alpha_2) for country in pycountry.countries] @@ -137,7 +137,7 @@ def _localized_country_list_inner(locale: str) -> t.List[t.Tuple[str, str]]: return [(code, name) for (name, code) in countries] -def localize_timezone(dt: datetime, tz: t.Union[None, str, tzinfo] = None) -> datetime: +def localize_timezone(dt: datetime, tz: Union[None, str, tzinfo] = None) -> datetime: """ Convert a datetime into the specified timezone, defaulting to user's timezone. diff --git a/src/baseframe/views.py b/src/baseframe/views.py index 0665dfc3..66f94233 100644 --- a/src/baseframe/views.py +++ b/src/baseframe/views.py @@ -2,8 +2,8 @@ import os import os.path -import typing as t from datetime import timedelta +from typing import Any, Optional from urllib.parse import urlparse import requests @@ -61,7 +61,7 @@ def networkbar_links() -> list: return networkbar_links_fetcher() -def asset_key(assets: t.List[str]) -> str: +def asset_key(assets: list[str]) -> str: """Convert multiple version specs into a URL-friendly key.""" return make_name( '-'.join(assets) @@ -74,7 +74,7 @@ def asset_key(assets: t.List[str]) -> str: ) -def gen_assets_url(assets: t.List[str]) -> str: +def gen_assets_url(assets: list[str]) -> str: """Create an asset bundle and return URL (helper for :func:`ext_assets`).""" # TODO: write test for this function try: @@ -108,7 +108,7 @@ def gen_assets_url(assets: t.List[str]) -> str: return bundle.urls()[0] -def ext_assets(assets: t.List[str]) -> str: +def ext_assets(assets: list[str]) -> str: """Return a URL to the required external assets.""" # XXX: External assets are deprecated, so this function serves them as internal # assets @@ -125,7 +125,7 @@ def asset_path(bundle_key: str) -> str: @baseframe.app_context_processor -def baseframe_context() -> t.Dict[str, t.Any]: +def baseframe_context() -> dict[str, Any]: """Add Baseframe helper functions to Jinja2 template context.""" return { 'networkbar_links': networkbar_links, @@ -135,7 +135,7 @@ def baseframe_context() -> t.Dict[str, t.Any]: @baseframe.route('/favicon.ico', subdomain='