Repository: aiogram/aiogram
Pull request #803 (2022-01-03 04:09:55)

Add class helper ChatAction
# Description

Add class helper ChatAction for constants that Telegram BotAPI uses in [send_chat_action](https://core.telegram.org/bots/api#sendchataction) request. 
In my opinion, this will help users and will also improve compatibility with 2.x version.

Difference from 2.x version:
• No class methods;
• Renamed `ChatActions` to `ChatAction`;
• Better class description.

## Type of change

Please delete options that are not relevant.

- [x] Documentation (typos, code examples or any documentation update)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

Testing code:
```
from aiogram.types.chat_action import ChatAction
from aiogram import types
import aiogram

assert types.ChatAction == ChatAction
assert aiogram.types.ChatAction == ChatAction
assert aiogram.types.ChatAction.TYPING == ChatAction.TYPING
assert ChatAction.TYPING == "typing"

print("Tests passed.")
```

**Test Configuration**:
* Python version: 3.8

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [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

ShiroNoHaga: Updated. Change using helper to using enum.Enum.
evgfilim1: > Small test code

@ShiroNoHaga, please add tests to `tests/` directory.
ShiroNoHaga: @evgfilim1 , added tests.
ShiroNoHaga: Updated formatting.
codecov[bot]: # [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/803?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#803](https://codecov.io/gh/aiogram/aiogram/pull/803?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (05946c0) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/6e39f9fada99906567d889c9618f65bc2f089c44?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (6e39f9f) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/803/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://codecov.io/gh/aiogram/aiogram/pull/803?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      #803   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          280       281    +1     
  Lines         7378      7394   +16     
=========================================
+ Hits          7378      7394   +16     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <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.

| [Impacted Files](https://codecov.io/gh/aiogram/aiogram/pull/803?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/types/\_\_init\_\_.py](https://codecov.io/gh/aiogram/aiogram/pull/803/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS90eXBlcy9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [aiogram/types/chat\_action.py](https://codecov.io/gh/aiogram/aiogram/pull/803/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS90eXBlcy9jaGF0X2FjdGlvbi5weQ==) | `100.00% <100.00%> (ø)` | |

ShiroNoHaga: Added pull request description to CHANGES.
Add test coverage.
github-actions[bot]: # :heavy_check_mark: Changelog found.

Thank you for adding a description of the changes
Olegt0rr: @evgfilim1, don't forget to add it to 2x->3x migration guide
evgfilim1: Hm... Where are the tests?
Olegt0rr: `enum.auto()` is not typehinted, so MyPy can't recognise `auto()`'s result as `str`
evgfilim1: GitHub, you're definitely crazy...
![image](https://user-images.githubusercontent.com/17182159/177056269-e0e666ad-53dd-43e4-b9a8-6c6d0ba2a702.png)
![image](https://user-images.githubusercontent.com/17182159/177056282-4e07a2bf-28e1-4ff4-8f14-3a89ba007d3e.png)


Patch:
diff --git a/CHANGES/803.feature b/CHANGES/803.feature
new file mode 100644
index 00000000..59ee6725
--- /dev/null
+++ b/CHANGES/803.feature
@@ -0,0 +1,3 @@
+Add class helper ChatAction for constants that Telegram BotAPI uses in sendChatAction request.
+In my opinion, this will help users and will also improve compatibility with 2.x version
+where similar class was called "ChatActions".
diff --git a/CHANGES/927.bugfix.rst b/CHANGES/927.bugfix.rst
new file mode 100644
index 00000000..c16ae945
--- /dev/null
+++ b/CHANGES/927.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed the ability to compare the state, now comparison to copy of the state will return `True`.
diff --git a/aiogram/dispatcher/fsm/state.py b/aiogram/dispatcher/fsm/state.py
index 6b29833b..6c76913f 100644
--- a/aiogram/dispatcher/fsm/state.py
+++ b/aiogram/dispatcher/fsm/state.py
@@ -54,6 +54,16 @@ class State:
             return True
         return raw_state == self.state
 
+    def __eq__(self, other: Any) -> bool:
+        if isinstance(other, self.__class__):
+            return self.state == other.state
+        if isinstance(other, str):
+            return self.state == other
+        return NotImplemented
+
+    def __hash__(self) -> int:
+        return hash(self.state)
+
 
 class StatesGroupMeta(type):
     __parent__: "Optional[Type[StatesGroup]]"
diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py
index ec04b855..e4a1819b 100644
--- a/aiogram/types/__init__.py
+++ b/aiogram/types/__init__.py
@@ -13,6 +13,7 @@ from .bot_command_scope_default import BotCommandScopeDefault
 from .callback_game import CallbackGame
 from .callback_query import CallbackQuery
 from .chat import Chat
+from .chat_action import ChatAction
 from .chat_administrator_rights import ChatAdministratorRights
 from .chat_invite_link import ChatInviteLink
 from .chat_join_request import ChatJoinRequest
@@ -275,6 +276,7 @@ __all__ = (
     "Game",
     "CallbackGame",
     "GameHighScore",
+    "ChatAction",
 )
 
 # Load typing forward refs for every TelegramObject
diff --git a/aiogram/types/chat_action.py b/aiogram/types/chat_action.py
new file mode 100644
index 00000000..619a0dc5
--- /dev/null
+++ b/aiogram/types/chat_action.py
@@ -0,0 +1,33 @@
+import enum
+
+from ..utils.enum import AutoName
+
+
+class ChatAction(AutoName):
+    """
+    This object represents bot actions.
+
+    Choose one, depending on what the user is about to receive:
+        • typing for text messages,
+        • upload_photo for photos,
+        • record_video or upload_video for videos,
+        • record_voice or upload_voice for voice notes,
+        • upload_document for general files,
+        • choose_sticker for stickers,
+        • find_location for location data,
+        • record_video_note or upload_video_note for video notes.
+
+    Source: https://core.telegram.org/bots/api#sendchataction
+    """
+
+    TYPING = enum.auto()
+    UPLOAD_PHOTO = enum.auto()
+    RECORD_VIDEO = enum.auto()
+    UPLOAD_VIDEO = enum.auto()
+    RECORD_VOICE = enum.auto()
+    UPLOAD_VOICE = enum.auto()
+    UPLOAD_DOCUMENT = enum.auto()
+    CHOOSE_STICKER = enum.auto()
+    FIND_LOCATION = enum.auto()
+    RECORD_VIDEO_NOTE = enum.auto()
+    UPLOAD_VIDEO_NOTE = enum.auto()
diff --git a/aiogram/utils/enum.py b/aiogram/utils/enum.py
new file mode 100644
index 00000000..b239ebc0
--- /dev/null
+++ b/aiogram/utils/enum.py
@@ -0,0 +1,8 @@
+import enum
+from typing import Any, List
+
+
+class AutoName(str, enum.Enum):
+    @staticmethod
+    def _generate_next_value_(name: str, start: int, count: int, last_values: List[Any]) -> str:
+        return name.lower()


Test patch:
diff --git a/tests/test_api/test_methods/test_send_chat_action.py b/tests/test_api/test_methods/test_send_chat_action.py
index 6b6454ae..98c9d016 100644
--- a/tests/test_api/test_methods/test_send_chat_action.py
+++ b/tests/test_api/test_methods/test_send_chat_action.py
@@ -1,6 +1,7 @@
 import pytest
 
 from aiogram.methods import Request, SendChatAction
+from aiogram.types import ChatAction
 from tests.mocked_bot import MockedBot
 
 pytestmark = pytest.mark.asyncio
@@ -22,3 +23,12 @@ class TestSendChatAction:
         request: Request = bot.get_request()
         assert request.method == "sendChatAction"
         assert response == prepare_result.result
+
+    async def test_chat_action_class(self, bot: MockedBot):
+        prepare_result = bot.add_result_for(SendChatAction, ok=True, result=True)
+
+        response: bool = await bot.send_chat_action(chat_id=42, action=ChatAction.TYPING)
+        request: Request = bot.get_request()
+        assert request.method == "sendChatAction"
+        assert request.data["action"] == "typing"
+        assert response == prepare_result.result
diff --git a/tests/test_dispatcher/test_filters/test_state.py b/tests/test_dispatcher/test_filters/test_state.py
index 2d8acda0..28bd578f 100644
--- a/tests/test_dispatcher/test_filters/test_state.py
+++ b/tests/test_dispatcher/test_filters/test_state.py
@@ -1,7 +1,9 @@
+from copy import copy
 from inspect import isclass
 
 import pytest
 
+from aiogram.dispatcher.event.handler import FilterObject
 from aiogram.dispatcher.filters import StateFilter
 from aiogram.dispatcher.fsm.state import State, StatesGroup
 from aiogram.types import Update
@@ -50,3 +52,23 @@ class TestStateFilter:
     async def test_filter(self, state, current_state, result):
         f = StateFilter(state=state)
         assert bool(await f(obj=Update(update_id=42), raw_state=current_state)) is result
+
+    @pytestmark
+    async def test_create_filter_from_state(self):
+        FilterObject(callback=State(state="state"))
+
+    @pytestmark
+    async def test_state_copy(self):
+        class SG(StatesGroup):
+            state = State()
+
+        assert SG.state == copy(SG.state)
+
+        assert SG.state == "SG:state"
+        assert "SG:state" == SG.state
+
+        assert State() == State()
+        assert SG.state != 1
+
+        states = {SG.state: "OK"}
+        assert states.get(copy(SG.state)) == "OK"