From b201f909a1e5c8f51643107995806409d19da22c Mon Sep 17 00:00:00 2001 From: DS/Charlie <82801887+ds-cbo@users.noreply.github.com> Date: Thu, 26 Jan 2023 11:18:16 +0100 Subject: [PATCH] Support passing lazy strings to utils.text functions (#1344) Currently, we get many of the following errors when trying to pass our Django code through mypy: error: Argument 1 to "capfirst" has incompatible type "_StrPromise"; expected "Optional[str]" [arg-type] Looking at the Django source code for capfirst it has been decorated with @keep_lazy_text meaning it also supports _StrPromise next to str. I've updated the typing of all similar functions to reflect this support. * Add bound typevar to bind output laziness to input laziness Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- django-stubs/utils/text.pyi | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/django-stubs/utils/text.pyi b/django-stubs/utils/text.pyi index 2c318e5ee..1b5f4c871 100644 --- a/django-stubs/utils/text.pyi +++ b/django-stubs/utils/text.pyi @@ -1,12 +1,15 @@ from collections.abc import Callable, Iterable, Iterator from io import BytesIO from re import Pattern +from typing import TypeVar, overload from django.db.models.base import Model -from django.utils.functional import SimpleLazyObject +from django.utils.functional import SimpleLazyObject, _StrOrPromise from django.utils.safestring import SafeString -def capfirst(x: str | None) -> str | None: ... +_StrOrPromiseT = TypeVar("_StrOrPromiseT", bound=_StrOrPromise) + +def capfirst(x: _StrOrPromiseT | None) -> _StrOrPromiseT | None: ... re_words: Pattern[str] re_chars: Pattern[str] @@ -14,7 +17,7 @@ re_tag: Pattern[str] re_newlines: Pattern[str] re_camel_case: Pattern[str] -def wrap(text: str, width: int) -> str: ... +def wrap(text: _StrOrPromiseT, width: int) -> _StrOrPromiseT: ... class Truncator(SimpleLazyObject): def __init__(self, text: Model | str) -> None: ... @@ -22,10 +25,13 @@ class Truncator(SimpleLazyObject): def chars(self, num: int, truncate: str | None = ..., html: bool = ...) -> str: ... def words(self, num: int, truncate: str | None = ..., html: bool = ...) -> str: ... -def get_valid_filename(name: str) -> str: ... +def get_valid_filename(name: _StrOrPromiseT) -> _StrOrPromiseT: ... +@overload def get_text_list(list_: list[str], last_word: str = ...) -> str: ... -def normalize_newlines(text: str) -> str: ... -def phone2numeric(phone: str) -> str: ... +@overload +def get_text_list(list_: list[_StrOrPromise], last_word: _StrOrPromise = ...) -> _StrOrPromise: ... +def normalize_newlines(text: _StrOrPromiseT) -> _StrOrPromiseT: ... +def phone2numeric(phone: _StrOrPromiseT) -> _StrOrPromiseT: ... def compress_string(s: bytes) -> bytes: ... class StreamingBuffer(BytesIO): @@ -37,9 +43,9 @@ def compress_sequence(sequence: Iterable[bytes]) -> Iterator[bytes]: ... smart_split_re: Pattern[str] def smart_split(text: str) -> Iterator[str]: ... -def unescape_entities(text: str) -> str: ... -def unescape_string_literal(s: str) -> str: ... -def slugify(value: str, allow_unicode: bool = ...) -> str: ... +def unescape_entities(text: _StrOrPromiseT) -> _StrOrPromiseT: ... +def unescape_string_literal(s: _StrOrPromiseT) -> _StrOrPromiseT: ... +def slugify(value: _StrOrPromiseT, allow_unicode: bool = ...) -> _StrOrPromiseT: ... def camel_case_to_spaces(value: str) -> str: ... format_lazy: Callable[..., str]