i18n: Deal with lazy strings more carefully.

This uses a more specific type `_StrPromise` to replace `Promise`
providing typing information for lazy translation strings.

In places where the callee evaluates the `_StrPromise` object in all
cases we simply force the evaluation with `str()`. This includes
`JsonableError` that ends up handled by the error handler middleware,
and `internal_send_stream_message` that depends on `check_stream_topic`,
requiring the `topic` to be evaluated anyway. In other siuations, the
callee is expected to be able to handle `StrPromise` explicitly.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
This commit is contained in:
Zixuan James Li
2022-08-08 13:53:11 -04:00
committed by Tim Abbott
parent ab9279aabe
commit bb9e80d7a2
8 changed files with 46 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
import datetime
from typing import Any, Dict, Iterable, List, Optional, Set, TypedDict
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, TypedDict
from django.conf import settings
from django.db import transaction
@@ -66,6 +66,9 @@ from zerver.models import (
)
from zerver.tornado.django_api import send_event
if TYPE_CHECKING:
from django.utils.functional import _StrPromise as StrPromise
def subscriber_info(user_id: int) -> Dict[str, Any]:
return {"id": user_id, "flags": ["read"]}
@@ -198,10 +201,10 @@ def send_message_moved_breadcrumbs(
user_profile: UserProfile,
old_stream: Stream,
old_topic: str,
old_thread_notification_string: Optional[str],
old_thread_notification_string: Optional["StrPromise"],
new_stream: Stream,
new_topic: Optional[str],
new_thread_notification_string: Optional[str],
new_thread_notification_string: Optional["StrPromise"],
changed_messages_count: int,
) -> None:
# Since moving content between streams is highly disruptive,

View File

@@ -844,7 +844,7 @@ def send_change_stream_permission_notification(
new_policy=new_policy_name,
)
internal_send_stream_message(
sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string
sender, stream, str(Realm.STREAM_EVENTS_NOTIFICATION_TOPIC), notification_string
)
@@ -1030,7 +1030,7 @@ def send_change_stream_post_policy_notification(
new_policy=Stream.POST_POLICIES[new_post_policy],
)
internal_send_stream_message(
sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string
sender, stream, str(Realm.STREAM_EVENTS_NOTIFICATION_TOPIC), notification_string
)
@@ -1153,7 +1153,7 @@ def do_rename_stream(stream: Stream, new_name: str, user_profile: UserProfile) -
internal_send_stream_message(
sender,
stream,
Realm.STREAM_EVENTS_NOTIFICATION_TOPIC,
str(Realm.STREAM_EVENTS_NOTIFICATION_TOPIC),
_("{user_name} renamed stream {old_stream_name} to {new_stream_name}.").format(
user_name=silent_mention_syntax_for_user(user_profile),
old_stream_name=f"**{old_name}**",
@@ -1190,7 +1190,7 @@ def send_change_stream_description_notification(
)
internal_send_stream_message(
sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string
sender, stream, str(Realm.STREAM_EVENTS_NOTIFICATION_TOPIC), notification_string
)
@@ -1276,7 +1276,7 @@ def send_change_stream_message_retention_days_notification(
summary_line=summary_line,
)
internal_send_stream_message(
sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string
sender, stream, str(Realm.STREAM_EVENTS_NOTIFICATION_TOPIC), notification_string
)

View File

@@ -140,7 +140,7 @@ class RegistrationForm(forms.Form):
if self.fields["password"].required and not check_password_strength(password):
# The frontend code tries to stop the user from submitting the form with a weak password,
# but if the user bypasses that protection, this error code path will run.
raise ValidationError(PASSWORD_TOO_WEAK_ERROR)
raise ValidationError(str(PASSWORD_TOO_WEAK_ERROR))
return password
@@ -275,7 +275,7 @@ class LoggingSetPasswordForm(SetPasswordForm):
if not check_password_strength(new_password):
# The frontend code tries to stop the user from submitting the form with a weak password,
# but if the user bypasses that protection, this error code path will run.
raise ValidationError(PASSWORD_TOO_WEAK_ERROR)
raise ValidationError(str(PASSWORD_TOO_WEAK_ERROR))
return new_password

View File

@@ -1,14 +1,16 @@
# See https://zulip.readthedocs.io/en/latest/subsystems/hotspots.html
# for documentation on this subsystem.
from typing import Dict, List
from typing import TYPE_CHECKING, Dict, List
from django.conf import settings
from django.utils.functional import Promise
from django.utils.translation import gettext_lazy
from zerver.models import UserHotspot, UserProfile
INTRO_HOTSPOTS: Dict[str, Dict[str, Promise]] = {
if TYPE_CHECKING:
from django.utils.functional import _StrPromise as StrPromise
INTRO_HOTSPOTS: Dict[str, Dict[str, "StrPromise"]] = {
"intro_streams": {
"title": gettext_lazy("Catch up on a stream"),
"description": gettext_lazy(
@@ -42,7 +44,7 @@ INTRO_HOTSPOTS: Dict[str, Dict[str, Promise]] = {
# We would most likely implement new hotspots in the future that aren't
# a part of the initial tutorial. To that end, classifying them into
# categories which are aggregated in ALL_HOTSPOTS, seems like a good start.
ALL_HOTSPOTS: Dict[str, Dict[str, Promise]] = {
ALL_HOTSPOTS: Dict[str, Dict[str, "StrPromise"]] = {
**INTRO_HOTSPOTS,
}

View File

@@ -1,10 +1,23 @@
import datetime
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict, TypeVar, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Tuple,
TypedDict,
TypeVar,
Union,
)
from django.utils.functional import Promise
from typing_extensions import NotRequired
if TYPE_CHECKING:
from django.utils.functional import _StrPromise as StrPromise
# See zerver/lib/validator.py for more details of Validators,
# including many examples
ResultT = TypeVar("ResultT")
@@ -37,9 +50,11 @@ class ProfileDataElementUpdateDict(TypedDict):
ProfileData = List[ProfileDataElement]
FieldElement = Tuple[int, Promise, Validator[ProfileDataElementValue], Callable[[Any], Any], str]
ExtendedFieldElement = Tuple[int, Promise, ExtendedValidator, Callable[[Any], Any], str]
UserFieldElement = Tuple[int, Promise, RealmUserValidator, Callable[[Any], Any], str]
FieldElement = Tuple[
int, "StrPromise", Validator[ProfileDataElementValue], Callable[[Any], Any], str
]
ExtendedFieldElement = Tuple[int, "StrPromise", ExtendedValidator, Callable[[Any], Any], str]
UserFieldElement = Tuple[int, "StrPromise", RealmUserValidator, Callable[[Any], Any], str]
ProfileFieldData = Dict[str, Union[Dict[str, str], str]]

View File

@@ -907,7 +907,7 @@ class Realm(models.Model):
def ensure_not_on_limited_plan(self) -> None:
if self.plan_type == Realm.PLAN_TYPE_LIMITED:
raise JsonableError(self.UPGRADE_TEXT_STANDARD)
raise JsonableError(str(self.UPGRADE_TEXT_STANDARD))
@property
def subdomain(self) -> str:
@@ -1872,7 +1872,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings):
}
def get_role_name(self) -> str:
return self.ROLE_ID_TO_NAME_MAP[self.role]
return str(self.ROLE_ID_TO_NAME_MAP[self.role])
def profile_data(self) -> ProfileData:
values = CustomProfileFieldValue.objects.filter(user_profile=self)
@@ -4505,7 +4505,9 @@ class CustomProfileField(models.Model):
FIELD_CONVERTERS: Dict[int, Callable[[Any], Any]] = {
item[0]: item[3] for item in ALL_FIELD_TYPES
}
FIELD_TYPE_CHOICES: List[Tuple[int, Promise]] = [(item[0], item[1]) for item in ALL_FIELD_TYPES]
FIELD_TYPE_CHOICES: List[Tuple[int, "StrPromise"]] = [
(item[0], item[1]) for item in ALL_FIELD_TYPES
]
field_type: int = models.PositiveSmallIntegerField(
choices=FIELD_TYPE_CHOICES,

View File

@@ -740,7 +740,7 @@ def send_messages_for_new_subscribers(
internal_prep_stream_message(
sender=sender,
stream=stream,
topic=Realm.STREAM_EVENTS_NOTIFICATION_TOPIC,
topic=str(Realm.STREAM_EVENTS_NOTIFICATION_TOPIC),
content=_(
"**{policy}** stream created by {user_name}. **Description:**"
).format(

View File

@@ -719,7 +719,7 @@ def create_user_backend(
pass
if not check_password_strength(password):
raise JsonableError(PASSWORD_TOO_WEAK_ERROR)
raise JsonableError(str(PASSWORD_TOO_WEAK_ERROR))
target_user = do_create_user(
email,