This commit is contained in:
Alex Root Junior 2023-06-10 20:39:03 +03:00
parent 1e2f8acf69
commit 10c6cc517d
No known key found for this signature in database
GPG key ID: 074C1D455EBEA4AC
3 changed files with 332 additions and 14 deletions

View file

@ -34,6 +34,8 @@ class UnsupportedKeywordArgument(DetailedAiogramError):
class TelegramAPIError(DetailedAiogramError):
label: str = "Telegram server says"
def __init__(
self,
method: TelegramMethod[TelegramType],
@ -44,11 +46,11 @@ class TelegramAPIError(DetailedAiogramError):
def __str__(self) -> str:
original_message = super().__str__()
return f"Telegram server says {original_message}"
return f"{self.label} - {original_message}"
class TelegramNetworkError(TelegramAPIError):
pass
label = "HTTP Client says"
class TelegramRetryAfter(TelegramAPIError):

View file

@ -49,7 +49,7 @@ class Text(Iterable[NodeType]):
@classmethod
def from_entities(cls, text: str, entities: List[MessageEntity]) -> "Text":
return Text(
return cls(
*_unparse_entities(
text=add_surrogates(text),
entities=sorted(entities, key=lambda item: item.offset) if entities else [],
@ -145,13 +145,16 @@ class Text(Iterable[NodeType]):
text, entities = self.render()
return markdown_decoration.unparse(text, entities)
def replace(self: Self, *args: Any, **kwargs: Any) -> Self:
return type(self)(*args, **{**self._params, **kwargs})
def as_pretty_string(self, indent: bool = False) -> str:
sep = ",\n" if indent else ", "
body = sep.join(
item.as_pretty_string(indent=indent) if isinstance(item, Text) else repr(item)
for item in self._body
)
params = sep.join(f"{k}={v!r}" for k, v in self._params.items())
params = sep.join(f"{k}={v!r}" for k, v in self._params.items() if v is not None)
args = []
if body:
@ -164,12 +167,6 @@ class Text(Iterable[NodeType]):
args_str = textwrap.indent("\n" + args_str + "\n", " ")
return f"{type(self).__name__}({args_str})"
def replace(self: Self, *args: Any, **kwargs: Any) -> Self:
return type(self)(*args, **{**self._params, **kwargs})
def __repr__(self) -> str:
return self.as_pretty_string()
def __add__(self, other: NodeType) -> "Text":
if isinstance(other, Text) and other.type == self.type and self._params == other._params:
return type(self)(*self, *other, **self._params)
@ -188,7 +185,7 @@ class Text(Iterable[NodeType]):
if not isinstance(item, slice):
raise TypeError("Can only be sliced")
if (item.start is None or item.start == 0) and item.stop is None:
return self
return self.replace(*self._body)
start = 0 if item.start is None else item.start
stop = len(self) if item.stop is None else item.stop
if start == stop:
@ -369,7 +366,7 @@ class Pre(Text):
type = MessageEntityType.PRE
def __init__(self, *body: NodeType, language: str, **params: Any) -> None:
def __init__(self, *body: NodeType, language: Optional[str] = None, **params: Any) -> None:
super().__init__(*body, language=language, **params)
@ -411,8 +408,8 @@ class CustomEmoji(Text):
type = MessageEntityType.CUSTOM_EMOJI
def __init__(self, *body: NodeType, emoji_id: str, **params: Any) -> None:
super().__init__(*body, emoji_id=emoji_id, **params)
def __init__(self, *body: NodeType, custom_emoji_id: str, **params: Any) -> None:
super().__init__(*body, custom_emoji_id=custom_emoji_id, **params)
NODE_TYPES: Dict[Optional[str], Type[Text]] = {

View file

@ -0,0 +1,319 @@
import pytest
from aiogram.enums import MessageEntityType
from aiogram.types import MessageEntity, User
from aiogram.utils.formatting import (
Bold,
BotCommand,
CashTag,
Code,
CustomEmoji,
Email,
HashTag,
Italic,
PhoneNumber,
Pre,
Spoiler,
Strikethrough,
Text,
TextLink,
TextMention,
Underline,
Url,
_apply_entity,
as_key_value,
as_line,
as_list,
as_marked_list,
as_marked_section,
as_numbered_list,
as_numbered_section,
as_section,
)
from aiogram.utils.text_decorations import html_decoration
class TestNode:
@pytest.mark.parametrize(
"node,result",
[
[
Text("test"),
"test",
],
[
HashTag("#test"),
"#test",
],
[
CashTag("$TEST"),
"$TEST",
],
[
BotCommand("/test"),
"/test",
],
[
Url("https://example.com"),
"https://example.com",
],
[
Email("test@example.com"),
"test@example.com",
],
[
PhoneNumber("test"),
"test",
],
[
Bold("test"),
"<b>test</b>",
],
[
Italic("test"),
"<i>test</i>",
],
[
Underline("test"),
"<u>test</u>",
],
[
Strikethrough("test"),
"<s>test</s>",
],
[
Spoiler("test"),
"<tg-spoiler>test</tg-spoiler>",
],
[
Code("test"),
"<code>test</code>",
],
[
Pre("test", language="python"),
'<pre><code class="language-python">test</code></pre>',
],
[
TextLink("test", url="https://example.com"),
'<a href="https://example.com">test</a>',
],
[
TextMention("test", user=User(id=42, is_bot=False, first_name="Test")),
'<a href="tg://user?id=42">test</a>',
],
[
CustomEmoji("test", custom_emoji_id="42"),
'<tg-emoji emoji-id="42">test</tg-emoji>',
],
],
)
def test_render_plain_only(self, node: Text, result: str):
text, entities = node.render()
if node.type:
assert len(entities) == 1
entity = entities[0]
assert entity.type == node.type
content = html_decoration.unparse(text, entities)
assert content == result
def test_render_text(self):
node = Text("Hello, ", "World", "!")
text, entities = node.render()
assert text == "Hello, World!"
assert not entities
def test_render_nested(self):
node = Text(
Text("Hello, ", Bold("World"), "!"),
"\n",
Text(Bold("This ", Underline("is"), " test", Italic("!"))),
"\n",
HashTag("#test"),
)
text, entities = node.render()
assert text == "Hello, World!\nThis is test!\n#test"
assert entities == [
MessageEntity(type="bold", offset=7, length=5),
MessageEntity(type="bold", offset=14, length=13),
MessageEntity(type="underline", offset=19, length=2),
MessageEntity(type="italic", offset=26, length=1),
MessageEntity(type="hashtag", offset=28, length=5),
]
def test_as_kwargs_default(self):
node = Text("Hello, ", Bold("World"), "!")
result = node.as_kwargs()
assert "text" in result
assert "entities" in result
assert "parse_mode" in result
def test_as_kwargs_custom(self):
node = Text("Hello, ", Bold("World"), "!")
result = node.as_kwargs(
text_key="caption",
entities_key="custom_entities",
parse_mode_key="custom_parse_mode",
)
assert "text" not in result
assert "caption" in result
assert "entities" not in result
assert "custom_entities" in result
assert "parse_mode" not in result
assert "custom_parse_mode" in result
def test_as_html(self):
node = Text("Hello, ", Bold("World"), "!")
assert node.as_html() == "Hello, <b>World</b>!"
def test_as_markdown(self):
node = Text("Hello, ", Bold("World"), "!")
assert node.as_markdown() == r"Hello, *World*\!"
def test_replace(self):
node0 = Text("test0", param0="test1")
node1 = node0.replace("test1", "test2", param1="test1")
assert node0._body != node1._body
assert node0._params != node1._params
assert "param1" not in node0._params
assert "param1" in node1._params
def test_add(self):
node0 = Text("Hello")
node1 = Bold("World")
node2 = node0 + Text(", ") + node1 + "!"
assert node0 != node2
assert node1 != node2
assert len(node0._body) == 1
assert len(node1._body) == 1
assert len(node2._body) == 3
text, entities = node2.render()
assert text == "Hello, World!"
def test_getitem_position(self):
node = Text("Hello, ", Bold("World"), "!")
with pytest.raises(TypeError):
node[2]
def test_getitem_empty_slice(self):
node = Text("Hello, ", Bold("World"), "!")
new_node = node[:]
assert new_node is not node
assert isinstance(new_node, Text)
assert new_node._body == node._body
def test_getitem_slice_zero(self):
node = Text("Hello, ", Bold("World"), "!")
new_node = node[2:2]
assert node is not new_node
assert isinstance(new_node, Text)
assert not new_node._body
def test_getitem_slice_simple(self):
node = Text("Hello, ", Bold("World"), "!")
new_node = node[2:10]
assert isinstance(new_node, Text)
text, entities = new_node.render()
assert text == "llo, Wor"
assert len(entities) == 1
assert entities[0].type == MessageEntityType.BOLD
def test_getitem_slice_inside_child(self):
node = Text("Hello, ", Bold("World"), "!")
new_node = node[8:10]
assert isinstance(new_node, Text)
text, entities = new_node.render()
assert text == "or"
assert len(entities) == 1
assert entities[0].type == MessageEntityType.BOLD
def test_getitem_slice_tail(self):
node = Text("Hello, ", Bold("World"), "!")
new_node = node[12:13]
assert isinstance(new_node, Text)
text, entities = new_node.render()
assert text == "!"
assert not entities
def test_from_entities(self):
# Most of the cases covered by text_decorations module
node = Strikethrough.from_entities(
text="test1 test2 test3 test4 test5 test6 test7",
entities=[
MessageEntity(type="bold", offset=6, length=29),
MessageEntity(type="underline", offset=12, length=5),
MessageEntity(type="italic", offset=24, length=5),
],
)
assert len(node._body) == 3
assert isinstance(node, Strikethrough)
rendered = node.as_html()
assert rendered == "<s>test1 <b>test2 <u>test3</u> test4 <i>test5</i> test6</b> test7</s>"
def test_pretty_string(self):
node = Strikethrough.from_entities(
text="X",
entities=[
MessageEntity(
type=MessageEntityType.CUSTOM_EMOJI,
offset=0,
length=1,
custom_emoji_id="42",
),
],
)
assert (
node.as_pretty_string(indent=True)
== """Strikethrough(
Text(
'X',
custom_emoji_id='42'
)
)"""
)
class TestUtils:
def test_apply_entity(self):
node = _apply_entity(
MessageEntity(type=MessageEntityType.BOLD, offset=0, length=4), "test"
)
assert isinstance(node, Bold)
assert node._body == ("test",)
def test_as_line(self):
node = as_line("test", "test", "test")
assert isinstance(node, Text)
assert len(node._body) == 4 # 3 + '\n'
def test_as_list(self):
node = as_list("test", "test", "test")
assert isinstance(node, Text)
assert len(node._body) == 5 # 3 + 2 * '\n' between lines
def test_as_marked_list(self):
node = as_marked_list("test 1", "test 2", "test 3")
assert node.as_html() == "- test 1\n- test 2\n- test 3"
def test_as_numbered_list(self):
node = as_numbered_list("test 1", "test 2", "test 3", start=5)
assert node.as_html() == "5. test 1\n6. test 2\n7. test 3"
def test_as_section(self):
node = as_section("title", "test 1", "test 2", "test 3")
assert node.as_html() == "title\ntest 1test 2test 3"
def test_as_marked_section(self):
node = as_marked_section("Section", "test 1", "test 2", "test 3")
assert node.as_html() == "Section\n- test 1\n- test 2\n- test 3"
def test_as_numbered_section(self):
node = as_numbered_section("Section", "test 1", "test 2", "test 3", start=5)
assert node.as_html() == "Section\n5. test 1\n6. test 2\n7. test 3"
def test_as_key_value(self):
node = as_key_value("key", "test 1")
assert node.as_html() == "<b>key:</b> test 1"