Added html/md_text properties to Message object and refactor I18n context

This commit is contained in:
Alex Root Junior 2021-09-23 23:45:22 +03:00
parent 481aec2144
commit c19cbc6a5f
10 changed files with 99 additions and 28 deletions

1
CHANGES/708.misc Normal file
View file

@ -0,0 +1 @@
Added :code:`html_text` and :code:`md_text` to Message object

1
CHANGES/709.misc Normal file
View file

@ -0,0 +1 @@
Refactored I18n, added context managers for I18n engine and current locale

View file

@ -37,5 +37,5 @@ __all__ = (
"md",
)
__version__ = "3.0.0a16"
__version__ = "3.0.0a17"
__api_version__ = "5.3"

View file

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, List, Optional, Union
from pydantic import Field
from aiogram.utils import helper
from aiogram.utils.text_decorations import TextDecoration, html_decoration, markdown_decoration
from .base import UNSET, TelegramObject
@ -259,6 +260,22 @@ class Message(TelegramObject):
return ContentType.UNKNOWN
def _unparse_entities(self, text_decoration: TextDecoration) -> str:
text = self.text or self.caption
if text is None:
raise TypeError("This message doesn't have any text.")
entities = self.entities or self.caption_entities
return text_decoration.unparse(text=text, entities=entities)
@property
def html_text(self) -> str:
return self._unparse_entities(html_decoration)
@property
def md_text(self) -> str:
return self._unparse_entities(markdown_decoration)
def reply_animation(
self,
animation: Union[InputFile, str],

View file

@ -1,14 +1,11 @@
from contextvars import ContextVar
from typing import Any, Optional
from typing import Any
from aiogram.utils.i18n.core import I18n
from aiogram.utils.i18n.lazy_proxy import LazyProxy
ctx_i18n: ContextVar[Optional[I18n]] = ContextVar("aiogram_ctx_i18n", default=None)
def get_i18n() -> I18n:
i18n = ctx_i18n.get()
i18n = I18n.get_current(no_error=True)
if i18n is None:
raise LookupError("I18n context is not set")
return i18n

View file

@ -1,24 +1,26 @@
import gettext
import os
from contextlib import contextmanager
from contextvars import ContextVar
from pathlib import Path
from typing import Dict, Optional, Tuple, Union
from typing import Dict, Generator, Optional, Tuple, Union
from aiogram.utils.i18n.lazy_proxy import LazyProxy
from aiogram.utils.mixins import ContextInstanceMixin
class I18n:
class I18n(ContextInstanceMixin["I18n"]):
def __init__(
self,
*,
path: Union[str, Path],
locale: str = "en",
default_locale: str = "en",
domain: str = "messages",
) -> None:
self.path = path
self.locale = locale
self.default_locale = default_locale
self.domain = domain
self.ctx_locale = ContextVar("aiogram_ctx_locale", default=locale)
self.ctx_locale = ContextVar("aiogram_ctx_locale", default=default_locale)
self.locales = self.find_locales()
@property
@ -29,6 +31,28 @@ class I18n:
def current_locale(self, value: str) -> None:
self.ctx_locale.set(value)
@contextmanager
def use_locale(self, locale: str) -> Generator[None, None, None]:
"""
Create context with specified locale
"""
ctx_token = self.ctx_locale.set(locale)
try:
yield
finally:
self.ctx_locale.reset(ctx_token)
@contextmanager
def context(self) -> Generator["I18n", None, None]:
"""
Use I18n context
"""
token = self.set_current(self)
try:
yield self
finally:
self.reset_current(token)
def find_locales(self) -> Dict[str, gettext.GNUTranslations]:
"""
Load all compiled locales from path

View file

@ -1,3 +1,4 @@
import logging
from abc import ABC, abstractmethod
from typing import Any, Awaitable, Callable, Dict, Optional, Set, cast
@ -9,9 +10,10 @@ except ImportError: # pragma: no cover
from aiogram import BaseMiddleware, Router
from aiogram.dispatcher.fsm.context import FSMContext
from aiogram.types import TelegramObject, User
from aiogram.utils.i18n.context import ctx_i18n
from aiogram.utils.i18n.core import I18n
logger = logging.getLogger(__name__)
class I18nMiddleware(BaseMiddleware, ABC):
"""
@ -60,17 +62,16 @@ class I18nMiddleware(BaseMiddleware, ABC):
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
self.i18n.current_locale = await self.get_locale(event=event, data=data)
current_locale = await self.get_locale(event=event, data=data) or self.i18n.default_locale
logger.debug("Detected locale %r", current_locale)
if self.i18n_key:
data[self.i18n_key] = self.i18n
if self.middleware_key:
data[self.middleware_key] = self
token = ctx_i18n.set(self.i18n)
try:
with self.i18n.context(), self.i18n.use_locale(current_locale):
return await handler(event, data)
finally:
ctx_i18n.reset(token)
@abstractmethod
async def get_locale(self, event: TelegramObject, data: Dict[str, Any]) -> str:
@ -118,10 +119,10 @@ class SimpleI18nMiddleware(I18nMiddleware):
event_from_user: Optional[User] = data.get("event_from_user", None)
if event_from_user is None:
return self.i18n.locale
return self.i18n.default_locale
locale = Locale.parse(event_from_user.language_code, sep="-")
if locale.language not in self.i18n.available_locales:
return self.i18n.locale
return self.i18n.default_locale
return cast(str, locale.language)

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "aiogram"
version = "3.0.0-alpha.16"
version = "3.0.0-alpha.17"
description = "Modern and fully asynchronous framework for Telegram Bot API"
authors = ["Alex Root Junior <jroot.junior@gmail.com>"]
license = "MIT"

View file

@ -42,6 +42,7 @@ from aiogram.types import (
Invoice,
Location,
MessageAutoDeleteTimerChanged,
MessageEntity,
PassportData,
PhotoSize,
Poll,
@ -638,3 +639,27 @@ class TestMessage:
assert isinstance(method, DeleteMessage)
assert method.chat_id == message.chat.id
assert method.message_id == message.message_id
@pytest.mark.parametrize(
"text,entities,correct",
[
["test", [MessageEntity(type="bold", offset=0, length=4)], True],
["", [], False],
],
)
def test_html_text(self, text, entities, correct):
message = Message(
message_id=42,
chat=Chat(id=42, type="private"),
date=datetime.datetime.now(),
text=text,
entities=entities,
)
if correct:
assert message.html_text
assert message.md_text
else:
with pytest.raises(TypeError):
assert message.html_text
with pytest.raises(TypeError):
assert message.md_text

View file

@ -7,7 +7,7 @@ from aiogram.dispatcher.fsm.context import FSMContext
from aiogram.dispatcher.fsm.storage.memory import MemoryStorage
from aiogram.types import Update, User
from aiogram.utils.i18n import ConstI18nMiddleware, FSMI18nMiddleware, I18n, SimpleI18nMiddleware
from aiogram.utils.i18n.context import ctx_i18n, get_i18n, gettext, lazy_gettext
from aiogram.utils.i18n.context import get_i18n, gettext, lazy_gettext
from tests.conftest import DATA_DIR
from tests.mocked_bot import MockedBot
@ -31,13 +31,21 @@ class TestI18nCore:
assert i18n.current_locale == "uk"
assert i18n.ctx_locale.get() == "uk"
def test_use_locale(self, i18n: I18n):
assert i18n.current_locale == "en"
with i18n.use_locale("uk"):
assert i18n.current_locale == "uk"
with i18n.use_locale("it"):
assert i18n.current_locale == "it"
assert i18n.current_locale == "uk"
assert i18n.current_locale == "en"
def test_get_i18n(self, i18n: I18n):
with pytest.raises(LookupError):
get_i18n()
token = ctx_i18n.set(i18n)
assert get_i18n() == i18n
ctx_i18n.reset(token)
with i18n.context():
assert get_i18n() == i18n
@pytest.mark.parametrize(
"locale,case,result",
@ -65,14 +73,11 @@ class TestI18nCore:
def test_gettext(self, i18n: I18n, locale: str, case: Dict[str, Any], result: str):
if locale is not None:
i18n.current_locale = locale
token = ctx_i18n.set(i18n)
try:
with i18n.context():
assert i18n.gettext(**case) == result
assert str(i18n.lazy_gettext(**case)) == result
assert gettext(**case) == result
assert str(lazy_gettext(**case)) == result
finally:
ctx_i18n.reset(token)
async def next_call(event, data):