diff --git a/aiogram/exceptions.py b/aiogram/exceptions.py
index 1c1e59fb..7ca7dcdd 100644
--- a/aiogram/exceptions.py
+++ b/aiogram/exceptions.py
@@ -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):
diff --git a/aiogram/utils/formatting.py b/aiogram/utils/formatting.py
index 45a19c9e..513d27fd 100644
--- a/aiogram/utils/formatting.py
+++ b/aiogram/utils/formatting.py
@@ -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]] = {
diff --git a/tests/test_utils/test_formatting.py b/tests/test_utils/test_formatting.py
new file mode 100644
index 00000000..5e14c4dc
--- /dev/null
+++ b/tests/test_utils/test_formatting.py
@@ -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"),
+ "test",
+ ],
+ [
+ Italic("test"),
+ "test",
+ ],
+ [
+ Underline("test"),
+ "test",
+ ],
+ [
+ Strikethrough("test"),
+ "test",
+ ],
+ [
+ Spoiler("test"),
+ "test",
+ ],
+ [
+ Pre("test", language="python"),
+ '
test',
+ ],
+ [
+ TextLink("test", url="https://example.com"),
+ 'test',
+ ],
+ [
+ TextMention("test", user=User(id=42, is_bot=False, first_name="Test")),
+ 'test',
+ ],
+ [
+ CustomEmoji("test", custom_emoji_id="42"),
+ '