mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-06 07:50:32 +00:00
Added html/md_text properties to Message object and refactor I18n context
This commit is contained in:
parent
481aec2144
commit
c19cbc6a5f
10 changed files with 99 additions and 28 deletions
1
CHANGES/708.misc
Normal file
1
CHANGES/708.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Added :code:`html_text` and :code:`md_text` to Message object
|
||||
1
CHANGES/709.misc
Normal file
1
CHANGES/709.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Refactored I18n, added context managers for I18n engine and current locale
|
||||
|
|
@ -37,5 +37,5 @@ __all__ = (
|
|||
"md",
|
||||
)
|
||||
|
||||
__version__ = "3.0.0a16"
|
||||
__version__ = "3.0.0a17"
|
||||
__api_version__ = "5.3"
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue