diff --git a/.github/workflows/pull_request_changelog.yml b/.github/workflows/pull_request_changelog.yml index 538ea1ed..f803153e 100644 --- a/.github/workflows/pull_request_changelog.yml +++ b/.github/workflows/pull_request_changelog.yml @@ -1,3 +1,4 @@ +name: "Check that changes are described" on: pull_request_target: types: @@ -8,31 +9,32 @@ on: - "unlabeled" jobs: - check_changes: + changes-required: runs-on: ubuntu-latest - name: "Check that changes is described" + if: "!contains(github.event.pull_request.labels.*.name, 'skip news')" steps: - - name: "Checkout code" + - name: Checkout code uses: actions/checkout@master with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: '0' + - name: Set up Python 3.10 uses: actions/setup-python@v2 with: python-version: "3.10" + - name: Install towncrier run: pip install towncrier - - name: "Check changelog" - if: "!contains(github.event.pull_request.labels.*.name, 'skip news')" + - name: Check changelog env: BASE_BRANCH: ${{ github.base_ref }} run: | git fetch --no-tags origin +refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH} towncrier check --compare-with origin/${BASE_BRANCH} - - name: Find Comment + - name: Find bot comment if: "always()" uses: peter-evans/find-comment@v2 id: fc @@ -41,7 +43,7 @@ jobs: comment-author: 'github-actions[bot]' body-includes: Changelog - - name: Create fail comment + - name: Ask for changelog if: "failure()" uses: peter-evans/create-or-update-comment@v2 with: @@ -57,7 +59,7 @@ jobs: Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments) - - name: Create success comment + - name: Changelog found if: "success()" uses: peter-evans/create-or-update-comment@v2 with: @@ -69,8 +71,19 @@ jobs: Thank you for adding a description of the changes + skip-news: + runs-on: ubuntu-latest + if: "contains(github.event.pull_request.labels.*.name, 'skip news')" + steps: + - name: Find bot comment + uses: peter-evans/find-comment@v2 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: Changelog + - name: Comment when docs is not needed - if: "contains(github.event.pull_request.labels.*.name, 'skip news')" uses: peter-evans/create-or-update-comment@v2 with: edit-mode: replace @@ -79,4 +92,4 @@ jobs: body: | # :corn: Changelog is not needed. - This PR does not require a changelog in due to the `skip news` label. + This PR does not require a changelog because `skip news` label is present. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 73eac1b8..ba68db9b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,9 +4,29 @@ on: push: branches: - dev-3.x + paths: + - ".github/workflows/tests.yml" + - "aiogram/**" + - "tests/**" + - ".coveragerc" + - ".flake8" + - "codecov.yaml" + - "mypy.ini" + - "poetry.lock" + - "pyproject.toml" pull_request: branches: - dev-3.x + paths: + - ".github/workflows/tests.yml" + - "aiogram/**" + - "tests/**" + - ".coveragerc" + - ".flake8" + - "codecov.yaml" + - "mypy.ini" + - "poetry.lock" + - "pyproject.toml" jobs: build: @@ -24,7 +44,7 @@ jobs: - '3.10' defaults: - # Windows is sucks. Force use bash instead of PowerShell + # Windows sucks. Force use bash instead of PowerShell run: shell: bash 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 @@ +Fixed the ability to compare the state, now comparison to copy of the state will return `True`. diff --git a/aiogram/fsm/state.py b/aiogram/fsm/state.py index a03f5238..8b341017 100644 --- a/aiogram/fsm/state.py +++ b/aiogram/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() 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_filters/test_state.py b/tests/test_filters/test_state.py index 6e7b08fe..84bc6a6c 100644 --- a/tests/test_filters/test_state.py +++ b/tests/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.filters import StateFilter from aiogram.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"