Repository: aiogram/aiogram
Pull request #1332 (2023-10-08 15:26:23)

Fix send_copy helper parse mode
# Description

If you set default parse mode on Bot instance, this parse mode applies on all calls with UNSET parse mode, even on `send_copy`, where entities are already applied and no parse mode is required. It even breaks copying regular messages with 'special' characters like `.` or `!` when `MarkdownV2` is set by default. 
Parse mode should not be sent, but we need to have an option to override it. So, by default `parse_mode` in `send_copy` should be `None.

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)

# How Has This Been Tested?

- [X] Added autotests
- [X] Created a Bot instance with default parse_mode = `MarkdownV2`

**Test Configuration**:
* Operating System: macOS
* Python version: 3.11

# Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes


github-actions[bot]: # :x: Changelog is required!

You need to add a brief description of the changes to the `CHANGES` directory.

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/tutorial.html#creating-news-fragments)
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aiogram/aiogram/pull/1332?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#1332](https://app.codecov.io/gh/aiogram/aiogram/pull/1332?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (05531e4) into [dev-3.x](https://app.codecov.io/gh/aiogram/aiogram/commit/67382553e5329ecc1e9340da7f8c020a856d35e9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (6738255) will **not change** coverage.
> Report is 2 commits behind head on dev-3.x.
> The diff coverage is `n/a`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/aiogram/aiogram/pull/1332/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://app.codecov.io/gh/aiogram/aiogram/pull/1332?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@            Coverage Diff            @@
##           dev-3.x     #1332   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          369       369           
  Lines         9429      9435    +6     
=========================================
+ Hits          9429      9435    +6     
```

| [Flag](https://app.codecov.io/gh/aiogram/aiogram/pull/1332/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/aiogram/aiogram/pull/1332/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | `100.00% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files](https://app.codecov.io/gh/aiogram/aiogram/pull/1332?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/types/message.py](https://app.codecov.io/gh/aiogram/aiogram/pull/1332?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS90eXBlcy9tZXNzYWdlLnB5) | `100.00% <ø> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/aiogram/aiogram/pull/1332/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

</details>

Patch:
diff --git a/CHANGES/1332.bugfix.rst b/CHANGES/1332.bugfix.rst
new file mode 100644
index 00000000..004cfd1d
--- /dev/null
+++ b/CHANGES/1332.bugfix.rst
@@ -0,0 +1,1 @@
+ Fixed ``parse_mode`` in ``send_copy`` helper. Disable by default.
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index da3e1f98..87798b7e 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -2803,6 +2803,7 @@ class Message(TelegramObject):
         reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None,
         allow_sending_without_reply: Optional[bool] = None,
         message_thread_id: Optional[int] = None,
+        parse_mode: Optional[str] = None,
     ) -> Union[
         ForwardMessage,
         SendAnimation,
@@ -2837,6 +2838,7 @@ class Message(TelegramObject):
         :param reply_markup:
         :param allow_sending_without_reply:
         :param message_thread_id:
+        :param parse_mode:
         :return:
         """
         from ..methods import (
@@ -2864,6 +2866,9 @@ class Message(TelegramObject):
             "reply_to_message_id": reply_to_message_id,
             "message_thread_id": message_thread_id,
             "allow_sending_without_reply": allow_sending_without_reply,
+            # when sending a copy, we don't need any parse mode
+            # because all entities are already prepared
+            "parse_mode": parse_mode,
         }
 
         if self.text:


Test patch:
diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py
index 0b600146..4e893d20 100644
--- a/tests/test_api/test_types/test_message.py
+++ b/tests/test_api/test_types/test_message.py
@@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Type, Union
 
 import pytest
 
+from aiogram.enums import ParseMode
 from aiogram.methods import (
     CopyMessage,
     DeleteMessage,
@@ -74,6 +75,7 @@ from aiogram.types import (
     VideoNote,
     Voice,
     WebAppData,
+    UNSET_PARSE_MODE,
 )
 from aiogram.types.message import ContentType, Message
 
@@ -688,10 +690,44 @@ class TestMessage:
             return
 
         method = message.send_copy(chat_id=42)
-        if method:
-            assert isinstance(method, expected_method)
+        assert isinstance(method, expected_method)
+        if hasattr(method, "parse_mode"):
+            # default parse mode in send_copy
+            assert method.parse_mode is None
         # TODO: Check additional fields
 
+    @pytest.mark.parametrize(
+        "custom_parse_mode",
+        [
+            UNSET_PARSE_MODE,
+            *list(ParseMode),
+        ],
+    )
+    @pytest.mark.parametrize(
+        "message,expected_method",
+        [
+            [TEST_MESSAGE_TEXT, SendMessage],
+            [TEST_MESSAGE_AUDIO, SendAudio],
+            [TEST_MESSAGE_ANIMATION, SendAnimation],
+            [TEST_MESSAGE_DOCUMENT, SendDocument],
+            [TEST_MESSAGE_PHOTO, SendPhoto],
+            [TEST_MESSAGE_VIDEO, SendVideo],
+            [TEST_MESSAGE_VOICE, SendVoice],
+        ],
+    )
+    def test_send_copy_custom_parse_mode(
+        self,
+        message: Message,
+        expected_method: Optional[Type[TelegramMethod]],
+        custom_parse_mode: Optional[str],
+    ):
+        method = message.send_copy(
+            chat_id=42,
+            parse_mode=custom_parse_mode,
+        )
+        assert isinstance(method, expected_method)
+        assert method.parse_mode == custom_parse_mode
+
     def test_edit_text(self):
         message = Message(
             message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()