Backport text decorations. Improve timeouts in tests. Improve TelegramAPIServer

This commit is contained in:
Alex Root Junior 2021-01-24 23:12:07 +02:00
parent 4292b3934f
commit 1a185928a2
11 changed files with 82 additions and 48 deletions

View file

@ -1,7 +1,7 @@
from dataclasses import dataclass
@dataclass
@dataclass(frozen=True)
class TelegramAPIServer:
"""
Base config for API Endpoints
@ -9,10 +9,11 @@ class TelegramAPIServer:
base: str
file: str
is_local: bool = False
def api_url(self, token: str, method: str) -> str:
"""
Generate URL for methods
Generate URL for API methods
:param token: Bot token
:param method: API method name (case insensitive)
@ -30,9 +31,15 @@ class TelegramAPIServer:
"""
return self.file.format(token=token, path=path)
@classmethod
def from_base(cls, base: str, is_local: bool = False) -> "TelegramAPIServer":
base = base.rstrip("/")
return cls(
base=f"{base}/bot{{token}}/{{method}}",
file=f"{base}/file/bot{{token}}/{{path}}",
is_local=is_local,
)
# Main API server
PRODUCTION = TelegramAPIServer(
base="https://api.telegram.org/bot{token}/{method}",
file="https://api.telegram.org/file/bot{token}/{path}",
)
PRODUCTION = TelegramAPIServer.from_base("https://api.telegram.org")

View file

@ -247,7 +247,7 @@ class Dispatcher(Router):
raise
async def feed_webhook_update(
self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: int = 55, **kwargs: Any
self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: float = 55, **kwargs: Any
) -> Optional[Dict[str, Any]]:
if not isinstance(update, Update): # Allow to use raw updates
update = Update(**update)

View file

@ -9,9 +9,9 @@ if TYPE_CHECKING: # pragma: no cover
from aiogram.types import MessageEntity
__all__ = (
"TextDecoration",
"HtmlDecoration",
"MarkdownDecoration",
"TextDecoration",
"html_decoration",
"markdown_decoration",
)
@ -57,14 +57,15 @@ class TextDecoration(ABC):
"""
result = "".join(
self._unparse_entities(
text, sorted(entities, key=lambda item: item.offset) if entities else []
self._add_surrogates(text),
sorted(entities, key=lambda item: item.offset) if entities else [],
)
)
return result
def _unparse_entities(
self,
text: str,
text: bytes,
entities: List[MessageEntity],
offset: Optional[int] = None,
length: Optional[int] = None,
@ -74,21 +75,31 @@ class TextDecoration(ABC):
length = length or len(text)
for index, entity in enumerate(entities):
if entity.offset < offset:
if entity.offset * 2 < offset:
continue
if entity.offset > offset:
yield self.quote(text[offset : entity.offset])
start = entity.offset
offset = entity.offset + entity.length
if entity.offset * 2 > offset:
yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2]))
start = entity.offset * 2
offset = entity.offset * 2 + entity.length * 2
sub_entities = list(filter(lambda e: e.offset < (offset or 0), entities[index + 1 :]))
sub_entities = list(
filter(lambda e: e.offset * 2 < (offset or 0), entities[index + 1 :])
)
yield self.apply_entity(
entity,
"".join(self._unparse_entities(text, sub_entities, offset=start, length=offset)),
)
if offset < length:
yield self.quote(text[offset:length])
yield self.quote(self._remove_surrogates(text[offset:length]))
@staticmethod
def _add_surrogates(text: str) -> bytes:
return text.encode("utf-16-le")
@staticmethod
def _remove_surrogates(text: bytes) -> str:
return text.decode("utf-16-le")
@abstractmethod
def link(self, value: str, link: str) -> str: # pragma: no cover
@ -153,11 +164,11 @@ class HtmlDecoration(TextDecoration):
return f"<s>{value}</s>"
def quote(self, value: str) -> str:
return html.escape(value)
return html.escape(value, quote=False)
class MarkdownDecoration(TextDecoration):
MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-|{}.!])")
MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
def link(self, value: str, link: str) -> str:
return f"[{value}]({link})"
@ -166,7 +177,7 @@ class MarkdownDecoration(TextDecoration):
return f"*{value}*"
def italic(self, value: str) -> str:
return f"_{value}_\r"
return f"_\r{value}_\r"
def code(self, value: str) -> str:
return f"`{value}`"
@ -178,7 +189,7 @@ class MarkdownDecoration(TextDecoration):
return f"```{language}\n{value}\n```"
def underline(self, value: str) -> str:
return f"__{value}__"
return f"__\r{value}__\r"
def strikethrough(self, value: str) -> str:
return f"~{value}~"

View file

@ -1,4 +1,4 @@
from aiogram.client.telegram import PRODUCTION
from aiogram.client.telegram import PRODUCTION, TelegramAPIServer
class TestAPIServer:
@ -9,3 +9,13 @@ class TestAPIServer:
def test_file_url(self):
file_url = PRODUCTION.file_url(token="42:TEST", path="path")
assert file_url == "https://api.telegram.org/file/bot42:TEST/path"
def test_from_base(self):
local_server = TelegramAPIServer.from_base("http://localhost:8081", is_local=True)
method_url = local_server.api_url("42:TEST", method="apiMethod")
file_url = local_server.file_url(token="42:TEST", path="path")
assert method_url == "http://localhost:8081/bot42:TEST/apiMethod"
assert file_url == "http://localhost:8081/file/bot42:TEST/path"
assert local_server.is_local

View file

@ -1,4 +1,5 @@
import pytest
from aiogram.methods import Close, Request
from tests.mocked_bot import MockedBot

View file

@ -1,4 +1,5 @@
import pytest
from aiogram.methods import CopyMessage, Request
from aiogram.types import MessageId
from tests.mocked_bot import MockedBot
@ -10,7 +11,9 @@ class TestCopyMessage:
prepare_result = bot.add_result_for(CopyMessage, ok=True, result=MessageId(message_id=42))
response: MessageId = await CopyMessage(
chat_id=42, from_chat_id=42, message_id=42,
chat_id=42,
from_chat_id=42,
message_id=42,
)
request: Request = bot.get_request()
assert request.method == "copyMessage"
@ -22,7 +25,9 @@ class TestCopyMessage:
prepare_result = bot.add_result_for(CopyMessage, ok=True, result=MessageId(message_id=42))
response: MessageId = await bot.copy_message(
chat_id=42, from_chat_id=42, message_id=42,
chat_id=42,
from_chat_id=42,
message_id=42,
)
request: Request = bot.get_request()
assert request.method == "copyMessage"

View file

@ -1,4 +1,5 @@
import pytest
from aiogram.methods import LogOut, Request
from tests.mocked_bot import MockedBot

View file

@ -1,4 +1,5 @@
import pytest
from aiogram.methods import Request, UnpinAllChatMessages
from tests.mocked_bot import MockedBot
@ -8,7 +9,9 @@ class TestUnpinAllChatMessages:
async def test_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(UnpinAllChatMessages, ok=True, result=True)
response: bool = await UnpinAllChatMessages(chat_id=42,)
response: bool = await UnpinAllChatMessages(
chat_id=42,
)
request: Request = bot.get_request()
assert request.method == "unpinAllChatMessages"
# assert request.data == {}
@ -18,7 +21,9 @@ class TestUnpinAllChatMessages:
async def test_bot_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(UnpinAllChatMessages, ok=True, result=True)
response: bool = await bot.unpin_all_chat_messages(chat_id=42,)
response: bool = await bot.unpin_all_chat_messages(
chat_id=42,
)
request: Request = bot.get_request()
assert request.method == "unpinAllChatMessages"
# assert request.data == {}

View file

@ -37,12 +37,12 @@ except ImportError:
async def simple_message_handler(message: Message):
await asyncio.sleep(1.5)
await asyncio.sleep(0.2)
return message.answer("ok")
async def invalid_message_handler(message: Message):
await asyncio.sleep(1.5)
await asyncio.sleep(0.2)
raise Exception(42)
@ -578,21 +578,11 @@ class TestDispatcher:
dispatcher = Dispatcher()
dispatcher.message.register(simple_message_handler)
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=2)
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.3)
assert isinstance(response, dict)
assert response["method"] == "sendMessage"
assert response["text"] == "ok"
# @pytest.mark.asyncio
# async def test_feed_webhook_update_fast_process_error(self, bot: MockedBot):
# dispatcher = Dispatcher()
# dispatcher.message_handler.register(invalid_message_handler)
#
# response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=2)
# assert isinstance(response, dict)
# assert response["method"] == "sendMessage"
# assert response["text"] == "ok"
@pytest.mark.asyncio
async def test_feed_webhook_update_slow_process(self, bot: MockedBot, recwarn):
warnings.simplefilter("always")
@ -604,9 +594,9 @@ class TestDispatcher:
"aiogram.dispatcher.dispatcher.Dispatcher._silent_call_request",
new_callable=CoroutineMock,
) as mocked_silent_call_request:
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=1)
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
assert response is None
await asyncio.sleep(1)
await asyncio.sleep(0.2)
mocked_silent_call_request.assert_awaited()
@pytest.mark.asyncio
@ -616,9 +606,9 @@ class TestDispatcher:
dispatcher = Dispatcher()
dispatcher.message.register(invalid_message_handler)
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=1)
response = await dispatcher.feed_webhook_update(bot, RAW_UPDATE, _timeout=0.1)
assert response is None
await asyncio.sleep(1)
await asyncio.sleep(0.1)
log_records = [rec.message for rec in caplog.records]
assert "Cause exception while process update" in log_records[0]

View file

@ -31,13 +31,13 @@ class TestMarkdown:
[text, ("test", "test"), None, "test test"],
[bold, ("test", "test"), " ", "*test test*"],
[hbold, ("test", "test"), " ", "<b>test test</b>"],
[italic, ("test", "test"), " ", "_test test_\r"],
[italic, ("test", "test"), " ", "_\rtest test_\r"],
[hitalic, ("test", "test"), " ", "<i>test test</i>"],
[code, ("test", "test"), " ", "`test test`"],
[hcode, ("test", "test"), " ", "<code>test test</code>"],
[pre, ("test", "test"), " ", "```test test```"],
[hpre, ("test", "test"), " ", "<pre>test test</pre>"],
[underline, ("test", "test"), " ", "__test test__"],
[underline, ("test", "test"), " ", "__\rtest test__\r"],
[hunderline, ("test", "test"), " ", "<u>test test</u>"],
[strikethrough, ("test", "test"), " ", "~test test~"],
[hstrikethrough, ("test", "test"), " ", "<s>test test</s>"],

View file

@ -53,7 +53,7 @@ class TestTextDecoration:
'<a href="https://aiogram.dev">test</a>',
],
[markdown_decoration, MessageEntity(type="bold", offset=0, length=5), "*test*"],
[markdown_decoration, MessageEntity(type="italic", offset=0, length=5), "_test_\r"],
[markdown_decoration, MessageEntity(type="italic", offset=0, length=5), "_\rtest_\r"],
[markdown_decoration, MessageEntity(type="code", offset=0, length=5), "`test`"],
[markdown_decoration, MessageEntity(type="pre", offset=0, length=5), "```test```"],
[
@ -61,7 +61,11 @@ class TestTextDecoration:
MessageEntity(type="pre", offset=0, length=5, language="python"),
"```python\ntest\n```",
],
[markdown_decoration, MessageEntity(type="underline", offset=0, length=5), "__test__"],
[
markdown_decoration,
MessageEntity(type="underline", offset=0, length=5),
"__\rtest__\r",
],
[
markdown_decoration,
MessageEntity(type="strikethrough", offset=0, length=5),
@ -210,7 +214,7 @@ class TestTextDecoration:
[
html_decoration,
"test te👍🏿st test",
[MessageEntity(type="bold", offset=5, length=6, url=None, user=None)],
[MessageEntity(type="bold", offset=5, length=8, url=None, user=None)],
"test <b>te👍🏿st</b> test",
],
[