diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 70f83574d35..ea1173d6986 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,13 +1,12 @@ name: Test Documentation Build on: pull_request: - branches: - - master - - doc-fixes + paths: + - telegram/** + - docs/** push: branches: - master - - doc-fixes jobs: test-sphinx-build: diff --git a/.github/workflows/test_official.yml b/.github/workflows/test_official.yml index 9b972694616..d4853781142 100644 --- a/.github/workflows/test_official.yml +++ b/.github/workflows/test_official.yml @@ -1,8 +1,9 @@ name: Bot API Tests on: pull_request: - branches: - - master + paths: + - telegram/** + - tests/** push: branches: - master diff --git a/.github/workflows/type_completeness.yml b/.github/workflows/type_completeness.yml index 14e84d4d3e1..ac3b9934e9b 100644 --- a/.github/workflows/type_completeness.yml +++ b/.github/workflows/type_completeness.yml @@ -1,8 +1,8 @@ name: Check Type Completeness on: pull_request: - branches: - - master + paths: + - telegram/** push: branches: - master diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 1f45f0ab895..682ceead685 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -1,13 +1,12 @@ name: Unit Tests on: pull_request: - branches: - - master - + paths: + - telegram/** + - tests/** push: branches: - master - schedule: # Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions - cron: '7 3 * * 1,5' diff --git a/README.rst b/README.rst index 993e7239689..07dca0d2cf7 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.0-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -93,7 +93,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **7.0** are supported. +All types and methods of the Telegram Bot API **7.1** are supported. Installing ========== diff --git a/README_RAW.rst b/README_RAW.rst index d61101d7cb7..75dec821cba 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.0-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **7.0** are supported. +All types and methods of the Telegram Bot API **7.1** are supported. Installing ========== diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index ed71b5f6760..ffa7107b89e 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -22,6 +22,7 @@ Available Types telegram.chat telegram.chatadministratorrights telegram.chatboost + telegram.chatboostadded telegram.chatboostremoved telegram.chatboostsource telegram.chatboostsourcegiftcode diff --git a/docs/source/telegram.chatboostadded.rst b/docs/source/telegram.chatboostadded.rst new file mode 100644 index 00000000000..b4551e75b84 --- /dev/null +++ b/docs/source/telegram.chatboostadded.rst @@ -0,0 +1,6 @@ +ChatBoostAdded +============== + +.. autoclass:: telegram.ChatBoostAdded + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index 228bd459bac..050a6d52b9e 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -77,3 +77,5 @@ .. |reply_quote| replace:: If set to :obj:`True`, the reply is sent as an actual reply to this message. If ``reply_to_message_id`` is passed, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. .. |do_quote| replace:: If set to :obj:`True`, the replied message is quoted. For a dict, it must be the output of :meth:`~telegram.Message.build_reply_arguments` to specify exact ``reply_parameters``. If ``reply_to_message_id`` or ``reply_parameters`` are passed, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. + +.. |non_optional_story_argument| replace:: As of this version, this argument is now required. In accordance with our `stability policy `__, the signature will be kept as optional for now, though they are mandatory and an error will be raised if you don't pass it. diff --git a/telegram/__init__.py b/telegram/__init__.py index 8e58edb33e8..162ba3d0edb 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -40,6 +40,7 @@ "Chat", "ChatAdministratorRights", "ChatBoost", + "ChatBoostAdded", "ChatBoostRemoved", "ChatBoostSource", "ChatBoostSourceGiftCode", @@ -242,6 +243,7 @@ from ._chatadministratorrights import ChatAdministratorRights from ._chatboost import ( ChatBoost, + ChatBoostAdded, ChatBoostRemoved, ChatBoostSource, ChatBoostSourceGiftCode, diff --git a/telegram/_bot.py b/telegram/_bot.py index 4ca0ff267b7..dc6c66aab7c 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -5250,10 +5250,10 @@ async def promote_chat_member( user_id (:obj:`int`): Unique identifier of the target user. is_anonymous (:obj:`bool`, optional): Pass :obj:`True`, if the administrator's presence in the chat is hidden. - can_manage_chat (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can - access the chat event log, chat statistics, boost list in channels, see channel - members, report spam messages, see anonymous administrators in supergroups and - ignore slow mode. Implied by any other administrator privilege. + can_manage_chat (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + access the chat event log, get boost list, see hidden supergroup and channel + members, report spam messages and ignore slow mode. Implied by any other + administrator privilege. .. versionadded:: 13.4 @@ -5285,15 +5285,15 @@ async def promote_chat_member( .. versionadded:: 20.0 can_post_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can - post stories in the channel; channels only. + post stories to the chat. .. versionadded:: 20.6 can_edit_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can - edit stories posted by other users; channels only. + edit stories posted by other users. .. versionadded:: 20.6 can_delete_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can - delete stories posted by other users; channels only. + delete stories posted by other users. .. versionadded:: 20.6 diff --git a/telegram/_chat.py b/telegram/_chat.py index e830e83c2f2..d308cc0f265 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -219,6 +219,16 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 + unrestrict_boost_count (:obj:`int`, optional): For supergroups, the minimum number of + boosts that a non-administrator user needs to add in order to ignore slow mode and chat + permissions. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION + custom_emoji_sticker_set_name (:obj:`str`, optional): For supergroups, the name of the + group's custom emoji sticker set. Custom emoji from this set can be used by all users + and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits @@ -352,6 +362,16 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 + unrestrict_boost_count (:obj:`int`): Optional. For supergroups, the minimum number of + boosts that a non-administrator user needs to add in order to ignore slow mode and chat + permissions. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION + custom_emoji_sticker_set_name (:obj:`str`): Optional. For supergroups, the name of the + group's custom emoji sticker set. Custom emoji from this set can be used by all users + and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: NEXT.VERSION .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups .. _accent colors: https://core.telegram.org/bots/api#accent-colors @@ -364,6 +384,7 @@ class Chat(TelegramObject): "background_custom_emoji_id", "bio", "can_set_sticker_set", + "custom_emoji_sticker_set_name", "description", "emoji_status_custom_emoji_id", "emoji_status_expiration_date", @@ -392,6 +413,7 @@ class Chat(TelegramObject): "sticker_set_name", "title", "type", + "unrestrict_boost_count", "username", ) @@ -446,6 +468,8 @@ def __init__( profile_accent_color_id: Optional[int] = None, profile_background_custom_emoji_id: Optional[str] = None, has_visible_history: Optional[bool] = None, + unrestrict_boost_count: Optional[int] = None, + custom_emoji_sticker_set_name: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -493,6 +517,8 @@ def __init__( self.background_custom_emoji_id: Optional[str] = background_custom_emoji_id self.profile_accent_color_id: Optional[int] = profile_accent_color_id self.profile_background_custom_emoji_id: Optional[str] = profile_background_custom_emoji_id + self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count + self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name self._id_attrs = (self.id,) diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py index a6ec888ecfe..35e1adaac0d 100644 --- a/telegram/_chatadministratorrights.py +++ b/telegram/_chatadministratorrights.py @@ -47,9 +47,8 @@ class ChatAdministratorRights(TelegramObject): Args: is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event - log, chat statistics, boost list in channels, see channel members, report spam - messages, see anonymous administrators in supergroups and ignore slow mode. - Implied by any other administrator privilege. + log, get boost list, see hidden supergroup and channel members, report spam messages + and ignore slow mode. Implied by any other administrator privilege. can_delete_messages (:obj:`bool`): :obj:`True`, if the administrator can delete messages of other users. can_manage_video_chats (:obj:`bool`): :obj:`True`, if the administrator can manage video @@ -70,18 +69,24 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_post_stories (:obj:`bool`, optional): :obj:`True`, if the administrator can post - stories in the channel; channels only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. .. versionadded:: 20.6 - can_edit_stories (:obj:`bool`, optional): :obj:`True`, if the administrator can edit - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit + stories posted by other users. .. versionadded:: 20.6 - can_delete_stories (:obj:`bool`, optional): :obj:`True`, if the administrator can delete - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. .. versionadded:: 20.6 + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. @@ -90,9 +95,8 @@ class ChatAdministratorRights(TelegramObject): Attributes: is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event - log, chat statistics, boost list in channels, see channel members, report spam - messages, see anonymous administrators in supergroups and ignore slow mode. - Implied by any other administrator privilege. + log, get boost list, see hidden supergroup and channel members, report spam messages + and ignore slow mode. Implied by any other administrator privilege. can_delete_messages (:obj:`bool`): :obj:`True`, if the administrator can delete messages of other users. can_manage_video_chats (:obj:`bool`): :obj:`True`, if the administrator can manage video @@ -113,18 +117,24 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_post_stories (:obj:`bool`): Optional. :obj:`True`, if the administrator can post - stories in the channel; channels only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. .. versionadded:: 20.6 - can_edit_stories (:obj:`bool`): Optional. :obj:`True`, if the administrator can edit - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit + stories posted by other users. .. versionadded:: 20.6 - can_delete_stories (:obj:`bool`): Optional. :obj:`True`, if the administrator can delete - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. .. versionadded:: 20.6 + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. @@ -179,13 +189,19 @@ def __init__( self.can_promote_members: bool = can_promote_members self.can_change_info: bool = can_change_info self.can_invite_users: bool = can_invite_users + # Not actually optionals but because of backwards compatability we pretend they are + if can_post_stories is None or can_edit_stories is None or can_delete_stories is None: + raise TypeError( + "As of vNEXT.VERSION can_post_stories, can_edit_stories and can_delete_stories" + " must be set in order to create this object." + ) + self.can_post_stories: bool = can_post_stories + self.can_edit_stories: bool = can_edit_stories + self.can_delete_stories: bool = can_delete_stories # Optionals self.can_post_messages: Optional[bool] = can_post_messages self.can_edit_messages: Optional[bool] = can_edit_messages self.can_pin_messages: Optional[bool] = can_pin_messages - self.can_post_stories: Optional[bool] = can_post_stories - self.can_edit_stories: Optional[bool] = can_edit_stories - self.can_delete_stories: Optional[bool] = can_delete_stories self.can_manage_topics: Optional[bool] = can_manage_topics self._id_attrs = ( diff --git a/telegram/_chatboost.py b/telegram/_chatboost.py index 71d299de0b3..f16cf9db0eb 100644 --- a/telegram/_chatboost.py +++ b/telegram/_chatboost.py @@ -34,6 +34,39 @@ from telegram import Bot +class ChatBoostAdded(TelegramObject): + """ + This object represents a service message about a user boosting a chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`boost_count` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + boost_count (:obj:`int`): Number of boosts added by the user. + + Attributes: + boost_count (:obj:`int`): Number of boosts added by the user. + + """ + + __slots__ = ("boost_count",) + + def __init__( + self, + boost_count: int, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.boost_count: int = boost_count + self._id_attrs = (self.boost_count,) + + self._freeze() + + class ChatBoostSource(TelegramObject): """ Base class for Telegram ChatBoostSource objects. It can be one of: diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 301130b7d79..6d2d35ce3f4 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -197,9 +197,8 @@ class ChatMemberAdministrator(ChatMember): is allowed to edit administrator privileges of that user. is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. - can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator - can access the chat event log, chat statistics, message statistics in - channels, see channel members, see anonymous administrators in supergroups + can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event + log, get boost list, see hidden supergroup and channel members, report spam messages and ignore slow mode. Implied by any other administrator privilege. can_delete_messages (:obj:`bool`): :obj:`True`, if the administrator can delete messages of other users. @@ -225,18 +224,24 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_post_stories (:obj:`bool`, optional): :obj:`True`, if the administrator can post - stories in the channel; channels only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. .. versionadded:: 20.6 - can_edit_stories (:obj:`bool`, optional): :obj:`True`, if the administrator can edit - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit + stories posted by other users. .. versionadded:: 20.6 - can_delete_stories (:obj:`bool`, optional): :obj:`True`, if the administrator can delete - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. .. versionadded:: 20.6 + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. @@ -252,9 +257,8 @@ class ChatMemberAdministrator(ChatMember): is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event - log, chat statistics, boost list in channels, see channel members, report spam - messages, see anonymous administrators in supergroups and ignore slow mode. - Implied by any other administrator privilege. + log, get boost list, see hidden supergroup and channel members, report spam messages + and ignore slow mode. Implied by any other administrator privilege. can_delete_messages (:obj:`bool`): :obj:`True`, if the administrator can delete messages of other users. can_manage_video_chats (:obj:`bool`): :obj:`True`, if the @@ -279,18 +283,24 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_post_stories (:obj:`bool`): Optional. :obj:`True`, if the administrator can post - stories in the channel; channels only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. .. versionadded:: 20.6 - can_edit_stories (:obj:`bool`): Optional. :obj:`True`, if the administrator can edit - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit + stories posted by other users. .. versionadded:: 20.6 - can_delete_stories (:obj:`bool`): Optional. :obj:`True`, if the administrator can delete - stories posted by other users; channels only. + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. .. versionadded:: 20.6 + .. versionchanged:: NEXT.VERSION + |non_optional_story_argument| can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only @@ -352,14 +362,21 @@ def __init__( self.can_promote_members: bool = can_promote_members self.can_change_info: bool = can_change_info self.can_invite_users: bool = can_invite_users + # Not actually optionals but because of backwards compatability we pretend they are + if can_post_stories is None or can_edit_stories is None or can_delete_stories is None: + raise TypeError( + "As of NEXT.VERSION can_post_stories, can_edit_stories and can_delete_stories " + "must be set in order to create this object." + ) + self.can_post_stories: bool = can_post_stories + self.can_edit_stories: bool = can_edit_stories + self.can_delete_stories: bool = can_delete_stories + # Optionals self.can_post_messages: Optional[bool] = can_post_messages self.can_edit_messages: Optional[bool] = can_edit_messages self.can_pin_messages: Optional[bool] = can_pin_messages self.can_manage_topics: Optional[bool] = can_manage_topics self.custom_title: Optional[str] = custom_title - self.can_post_stories: Optional[bool] = can_post_stories - self.can_edit_stories: Optional[bool] = can_edit_stories - self.can_delete_stories: Optional[bool] = can_delete_stories class ChatMemberMember(ChatMember): diff --git a/telegram/_message.py b/telegram/_message.py index b94210a5efa..c8662a12123 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, TypedDict, Union from telegram._chat import Chat +from telegram._chatboost import ChatBoostAdded from telegram._dice import Dice from telegram._files.animation import Animation from telegram._files.audio import Audio @@ -521,6 +522,18 @@ class Message(MaybeInaccessibleMessage): message for forwarded messages .. versionadded:: 20.8 + reply_to_story (:class:`telegram.Story`, optional): For replies to a story, the original + story. + + .. versionadded:: NEXT.VERSION + boost_added (:class:`telegram.ChatBoostAdded`, optional): Service message: user boosted + the chat. + + .. versionadded:: NEXT.VERSION + sender_boost_count (:obj:`int`, optional): If the sender of the + message boosted the chat, the number of boosts added by the user. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -787,10 +800,22 @@ class Message(MaybeInaccessibleMessage): message, the quoted part of the message. .. versionadded:: 20.8 - forward_origin (:class:`telegram.MessageOrigin`, optional): Information about the original + forward_origin (:class:`telegram.MessageOrigin`): Optional. Information about the original message for forwarded messages .. versionadded:: 20.8 + reply_to_story (:class:`telegram.Story`): Optional. For replies to a story, the original + story. + + .. versionadded:: NEXT.VERSION + boost_added (:class:`telegram.ChatBoostAdded`): Optional. Service message: user boosted + the chat. + + .. versionadded:: NEXT.VERSION + sender_boost_count (:obj:`int`): Optional. If the sender of the + message boosted the chat, the number of boosts added by the user. + + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -807,6 +832,7 @@ class Message(MaybeInaccessibleMessage): "animation", "audio", "author_signature", + "boost_added", "caption", "caption_entities", "channel_chat_created", @@ -857,6 +883,8 @@ class Message(MaybeInaccessibleMessage): "quote", "reply_markup", "reply_to_message", + "reply_to_story", + "sender_boost_count", "sender_chat", "sticker", "story", @@ -953,6 +981,9 @@ def __init__( external_reply: Optional["ExternalReplyInfo"] = None, quote: Optional["TextQuote"] = None, forward_origin: Optional["MessageOrigin"] = None, + reply_to_story: Optional[Story] = None, + boost_added: Optional[ChatBoostAdded] = None, + sender_boost_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1045,6 +1076,9 @@ def __init__( self.external_reply: Optional[ExternalReplyInfo] = external_reply self.quote: Optional[TextQuote] = quote self.forward_origin: Optional[MessageOrigin] = forward_origin + self.reply_to_story: Optional[Story] = reply_to_story + self.boost_added: Optional[ChatBoostAdded] = boost_added + self.sender_boost_count: Optional[int] = sender_boost_count self._effective_attachment = DEFAULT_NONE @@ -1185,6 +1219,8 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]: data["external_reply"] = ExternalReplyInfo.de_json(data.get("external_reply"), bot) data["quote"] = TextQuote.de_json(data.get("quote"), bot) data["forward_origin"] = MessageOrigin.de_json(data.get("forward_origin"), bot) + data["reply_to_story"] = Story.de_json(data.get("reply_to_story"), bot) + data["boost_added"] = ChatBoostAdded.de_json(data.get("boost_added"), bot) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility diff --git a/telegram/_story.py b/telegram/_story.py index cdf6dd6ad11..42e4ae06d5e 100644 --- a/telegram/_story.py +++ b/telegram/_story.py @@ -18,24 +18,65 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object related to a Telegram Story.""" -from typing import Optional +from typing import TYPE_CHECKING, Optional +from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict +if TYPE_CHECKING: + from telegram import Bot + class Story(TelegramObject): """ - This object represents a message about a forwarded story in the chat. Currently holds no - information. + This object represents a story. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat` and :attr:`id` are equal. .. versionadded:: 20.5 + .. versionchanged:: NEXT.VERSION + Added attributes :attr:`chat` and :attr:`id` and equality based on them. + + Args: + chat (:class:`telegram.Chat`): Chat that posted the story. + id (:obj:`int`): Unique identifier for the story in the chat. + + Attributes: + chat (:class:`telegram.Chat`): Chat that posted the story. + id (:obj:`int`): Unique identifier for the story in the chat. + """ - __slots__ = () + __slots__ = ( + "chat", + "id", + ) - def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + def __init__( + self, + chat: Chat, + id: int, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: super().__init__(api_kwargs=api_kwargs) + self.chat: Chat = chat + self.id: int = id + + self._id_attrs = (self.chat, self.id) self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Story"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["chat"] = Chat.de_json(data.get("chat", {}), bot) + return super().de_json(data=data, bot=bot) diff --git a/telegram/constants.py b/telegram/constants.py index 01cab88d072..ec5723d33e9 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -142,7 +142,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=0) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=1) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -1705,6 +1705,11 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.animation`.""" AUDIO = "audio" """:obj:`str`: Messages with :attr:`telegram.Message.audio`.""" + BOOST_ADDED = "boost_added" + """:obj:`str`: Messages with :attr:`telegram.Message.boost_added`. + + .. versionadded:: NEXT.VERSION + """ CHANNEL_CHAT_CREATED = "channel_chat_created" """:obj:`str`: Messages with :attr:`telegram.Message.channel_chat_created`.""" CHAT_SHARED = "chat_shared" @@ -1802,6 +1807,16 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.poll`.""" PROXIMITY_ALERT_TRIGGERED = "proximity_alert_triggered" """:obj:`str`: Messages with :attr:`telegram.Message.proximity_alert_triggered`.""" + REPLY_TO_STORY = "reply_to_story" + """:obj:`str`: Messages with :attr:`telegram.Message.reply_to_story`. + + .. versionadded:: NEXT.VERSION + """ + SENDER_BOOST_COUNT = "sender_boost_count" + """:obj:`str`: Messages with :attr:`telegram.Message.sender_boost_count`. + + .. versionadded:: NEXT.VERSION + """ STICKER = "sticker" """:obj:`str`: Messages with :attr:`telegram.Message.sticker`.""" STORY = "story" diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index d78fa0d979f..9754569dcef 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -41,6 +41,7 @@ "ANIMATION", "ATTACHMENT", "AUDIO", + "BOOST_ADDED", "CAPTION", "CHAT", "COMMAND", @@ -60,6 +61,8 @@ "POLL", "PREMIUM_USER", "REPLY", + "REPLY_TO_STORY", + "SENDER_BOOST_COUNT", "STORY", "SUCCESSFUL_PAYMENT", "TEXT", @@ -2789,3 +2792,36 @@ def filter(self, message: Message) -> bool: VOICE = _Voice("filters.VOICE") """Messages that contain :attr:`telegram.Message.voice`.""" + + +class _ReplyToStory(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.reply_to_story) + + +REPLY_TO_STORY = _ReplyToStory(name="filters.REPLY_TO_STORY") +"""Messages that contain :attr:`telegram.Message.reply_to_story`.""" + + +class _BoostAdded(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.boost_added) + + +BOOST_ADDED = _BoostAdded(name="filters.BOOST_ADDED") +"""Messages that contain :attr:`telegram.Message.boost_added`.""" + + +class _SenderBoostCount(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.sender_boost_count) + + +SENDER_BOOST_COUNT = _SenderBoostCount(name="filters.SENDER_BOOST_COUNT") +"""Messages that contain :attr:`telegram.Message.sender_boost_count`.""" diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index 3a0f860d81b..bcd1980914e 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -2700,3 +2700,24 @@ def test_filters_giveaway_winners(self, update): update.message.giveaway_winners = "test" assert filters.GIVEAWAY_WINNERS.check_update(update) assert str(filters.GIVEAWAY_WINNERS) == "filters.GIVEAWAY_WINNERS" + + def test_filters_reply_to_story(self, update): + assert not filters.REPLY_TO_STORY.check_update(update) + + update.message.reply_to_story = "test" + assert filters.REPLY_TO_STORY.check_update(update) + assert str(filters.REPLY_TO_STORY) == "filters.REPLY_TO_STORY" + + def test_filters_boost_added(self, update): + assert not filters.BOOST_ADDED.check_update(update) + + update.message.boost_added = "test" + assert filters.BOOST_ADDED.check_update(update) + assert str(filters.BOOST_ADDED) == "filters.BOOST_ADDED" + + def test_filters_sender_boost_count(self, update): + assert not filters.SENDER_BOOST_COUNT.check_update(update) + + update.message.sender_boost_count = "test" + assert filters.SENDER_BOOST_COUNT.check_update(update) + assert str(filters.SENDER_BOOST_COUNT) == "filters.SENDER_BOOST_COUNT" diff --git a/tests/test_chat.py b/tests/test_chat.py index 514b5f4e7b0..2755853e1f7 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -72,6 +72,8 @@ def chat(bot): background_custom_emoji_id=TestChatBase.background_custom_emoji_id, profile_accent_color_id=TestChatBase.profile_accent_color_id, profile_background_custom_emoji_id=TestChatBase.profile_background_custom_emoji_id, + unrestrict_boost_count=TestChatBase.unrestrict_boost_count, + custom_emoji_sticker_set_name=TestChatBase.custom_emoji_sticker_set_name, ) chat.set_bot(bot) chat._unfreeze() @@ -115,6 +117,8 @@ class TestChatBase: background_custom_emoji_id = "background_custom_emoji_id" profile_accent_color_id = 2 profile_background_custom_emoji_id = "profile_background_custom_emoji_id" + unrestrict_boost_count = 100 + custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name" class TestChatWithoutRequest(TestChatBase): @@ -156,6 +160,8 @@ def test_de_json(self, bot): "background_custom_emoji_id": self.background_custom_emoji_id, "profile_accent_color_id": self.profile_accent_color_id, "profile_background_custom_emoji_id": self.profile_background_custom_emoji_id, + "unrestrict_boost_count": self.unrestrict_boost_count, + "custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name, } chat = Chat.de_json(json_dict, bot) @@ -194,6 +200,8 @@ def test_de_json(self, bot): assert chat.background_custom_emoji_id == self.background_custom_emoji_id assert chat.profile_accent_color_id == self.profile_accent_color_id assert chat.profile_background_custom_emoji_id == self.profile_background_custom_emoji_id + assert chat.unrestrict_boost_count == self.unrestrict_boost_count + assert chat.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { @@ -257,6 +265,8 @@ def test_to_dict(self, chat): chat_dict["profile_background_custom_emoji_id"] == chat.profile_background_custom_emoji_id ) + assert chat_dict["custom_emoji_sticker_set_name"] == chat.custom_emoji_sticker_set_name + assert chat_dict["unrestrict_boost_count"] == chat.unrestrict_boost_count def test_always_tuples_attributes(self): chat = Chat( diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py index 4b49792b098..c7b30be9237 100644 --- a/tests/test_chatadministratorrights.py +++ b/tests/test_chatadministratorrights.py @@ -95,11 +95,42 @@ def test_to_dict(self, chat_admin_rights): assert admin_rights_dict["can_delete_stories"] == car.can_delete_stories def test_equality(self): - a = ChatAdministratorRights(True, *((False,) * 11)) - b = ChatAdministratorRights(True, *((False,) * 11)) - c = ChatAdministratorRights(*(False,) * 12) - d = ChatAdministratorRights(True, True, *((False,) * 10)) - e = ChatAdministratorRights(True, True, *((False,) * 10)) + a = ChatAdministratorRights( + True, + *((False,) * 11), + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) + b = ChatAdministratorRights( + True, + *((False,) * 11), + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) + c = ChatAdministratorRights( + *(False,) * 12, + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) + d = ChatAdministratorRights( + True, + True, + *((False,) * 10), + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) + e = ChatAdministratorRights( + True, + True, + *((False,) * 10), + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) assert a == b assert hash(a) == hash(b) @@ -115,7 +146,20 @@ def test_equality(self): assert hash(d) == hash(e) def test_all_rights(self): - f = ChatAdministratorRights(True, True, True, True, True, True, True, True, True) + f = ChatAdministratorRights( + True, + True, + True, + True, + True, + True, + True, + True, + True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + ) t = ChatAdministratorRights.all_rights() # if the dirs are the same, the attributes will all be there assert dir(f) == dir(t) @@ -127,7 +171,20 @@ def test_all_rights(self): assert f != t def test_no_rights(self): - f = ChatAdministratorRights(False, False, False, False, False, False, False, False, False) + f = ChatAdministratorRights( + False, + False, + False, + False, + False, + False, + False, + False, + False, + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) t = ChatAdministratorRights.no_rights() # if the dirs are the same, the attributes will all be there assert dir(f) == dir(t) @@ -137,3 +194,19 @@ def test_no_rights(self): assert t[key] is False # and as a finisher, make sure the default is different. assert f != t + + def test_depreciation_typeerror(self): + with pytest.raises(TypeError, match="must be set in order"): + ChatAdministratorRights( + *(False,) * 12, + ) + with pytest.raises(TypeError, match="must be set in order"): + ChatAdministratorRights(*(False,) * 12, can_edit_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatAdministratorRights(*(False,) * 12, can_post_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatAdministratorRights(*(False,) * 12, can_delete_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatAdministratorRights(*(False,) * 12, can_edit_stories=True, can_post_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatAdministratorRights(*(False,) * 12, can_delete_stories=True, can_post_stories=True) diff --git a/tests/test_chatboost.py b/tests/test_chatboost.py index ee0a5c9e3c3..bc33e1fe21e 100644 --- a/tests/test_chatboost.py +++ b/tests/test_chatboost.py @@ -24,6 +24,7 @@ from telegram import ( Chat, ChatBoost, + ChatBoostAdded, ChatBoostRemoved, ChatBoostSource, ChatBoostSourceGiftCode, @@ -542,3 +543,42 @@ class TestUserChatBoostsWithRequest(ChatBoostDefaults): async def test_get_user_chat_boosts(self, bot, channel_id, chat_id): chat_boosts = await bot.get_user_chat_boosts(channel_id, chat_id) assert isinstance(chat_boosts, UserChatBoosts) + + +class TestChatBoostAddedWithoutRequest: + boost_count = 100 + + def test_slot_behaviour(self): + action = ChatBoostAdded(8) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + json_dict = {"boost_count": self.boost_count} + chat_boost_added = ChatBoostAdded.de_json(json_dict, None) + assert chat_boost_added.api_kwargs == {} + + assert chat_boost_added.boost_count == self.boost_count + + def test_to_dict(self): + chat_boost_added = ChatBoostAdded(self.boost_count) + chat_boost_added_dict = chat_boost_added.to_dict() + + assert isinstance(chat_boost_added_dict, dict) + assert chat_boost_added_dict["boost_count"] == self.boost_count + + def test_equality(self): + a = ChatBoostAdded(100) + b = ChatBoostAdded(100) + c = ChatBoostAdded(50) + d = Chat(1, "") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 643f0e3b018..90ea90294b7 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -150,8 +150,13 @@ def make_json_dict(instance: ChatMember, include_optional_args: bool = False) -> val = val.to_dict() json_dict[param.name] = val - # If we want to test all args (for de_json)- - elif param.default is not inspect.Parameter.empty and include_optional_args: + # If we want to test all args (for de_json) + # or if the param is optional but for backwards compatability + elif ( + param.default is not inspect.Parameter.empty + and include_optional_args + or param.name in ["can_delete_stories", "can_post_stories", "can_edit_stories"] + ): json_dict[param.name] = val return json_dict @@ -297,3 +302,19 @@ def test_equality(self, chat_member_type): assert c != e assert hash(c) != hash(e) + + def test_deprecation_typeerror(self, chat_member_type): + with pytest.raises(TypeError, match="must be set in order"): + ChatMemberAdministrator( + *(False,) * 12, + ) + with pytest.raises(TypeError, match="must be set in order"): + ChatMemberAdministrator(*(False,) * 12, can_edit_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatMemberAdministrator(*(False,) * 12, can_post_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatMemberAdministrator(*(False,) * 12, can_delete_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatMemberAdministrator(*(False,) * 12, can_edit_stories=True, can_post_stories=True) + with pytest.raises(TypeError, match="must be set in order"): + ChatMemberAdministrator(*(False,) * 12, can_delete_stories=True, can_post_stories=True) diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index 4e73526d0e0..8f9c405c06d 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -64,6 +64,9 @@ def new_chat_member(user): True, True, True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, ) @@ -264,10 +267,19 @@ def test_difference_required(self, user, chat): @pytest.mark.parametrize( "optional_attribute", # This gives the names of all optional arguments of ChatMember + # skipping stories names because they aren't optional even though we pretend they are [ name for name, param in inspect.signature(ChatMemberAdministrator).parameters.items() - if name not in ["self", "api_kwargs"] and param.default != inspect.Parameter.empty + if name + not in [ + "self", + "api_kwargs", + "can_delete_stories", + "can_post_stories", + "can_edit_stories", + ] + and param.default != inspect.Parameter.empty ], ) def test_difference_optionals(self, optional_attribute, user, chat): @@ -276,8 +288,22 @@ def test_difference_optionals(self, optional_attribute, user, chat): old_value = "old_value" new_value = "new_value" trues = tuple(True for _ in range(9)) - old_chat_member = ChatMemberAdministrator(user, *trues, **{optional_attribute: old_value}) - new_chat_member = ChatMemberAdministrator(user, *trues, **{optional_attribute: new_value}) + old_chat_member = ChatMemberAdministrator( + user, + *trues, + **{optional_attribute: old_value}, + can_delete_stories=True, + can_edit_stories=True, + can_post_stories=True, + ) + new_chat_member = ChatMemberAdministrator( + user, + *trues, + **{optional_attribute: new_value}, + can_delete_stories=True, + can_edit_stories=True, + can_post_stories=True, + ) chat_member_updated = ChatMemberUpdated( chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member ) diff --git a/tests/test_keyboardbuttonrequest.py b/tests/test_keyboardbuttonrequest.py index c8f620ff5d8..8678fd08d8e 100644 --- a/tests/test_keyboardbuttonrequest.py +++ b/tests/test_keyboardbuttonrequest.py @@ -106,10 +106,30 @@ class TestKeyboardButtonRequestChatBase: chat_has_username = True chat_is_created = False user_administrator_rights = ChatAdministratorRights( - True, False, True, False, True, False, True, False + True, + False, + True, + False, + True, + False, + True, + False, + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, ) bot_administrator_rights = ChatAdministratorRights( - True, False, True, False, True, False, True, False + True, + False, + True, + False, + True, + False, + True, + False, + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, ) bot_is_member = True diff --git a/tests/test_message.py b/tests/test_message.py index f79505035b2..b98f36b4577 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -26,6 +26,7 @@ Audio, Bot, Chat, + ChatBoostAdded, ChatShared, Contact, Dice, @@ -129,7 +130,7 @@ def message(bot): }, {"photo": [PhotoSize("photo_id", "unique_id", 50, 50)], "caption": "photo_file"}, {"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR)}, - {"story": Story()}, + {"story": Story(Chat(1, Chat.PRIVATE), 0)}, {"video": Video("video_id", "unique_id", 12, 12, 12), "caption": "video_file"}, {"voice": Voice("voice_id", "unique_id", 5)}, {"video_note": VideoNote("video_note_id", "unique_id", 20, 12)}, @@ -259,6 +260,9 @@ def message(bot): }, {"quote": TextQuote("a text quote", 1)}, {"forward_origin": MessageOriginChat(datetime.utcnow(), Chat(1, Chat.PRIVATE))}, + {"reply_to_story": Story(Chat(1, Chat.PRIVATE), 0)}, + {"boost_added": ChatBoostAdded(100)}, + {"sender_boost_count": 1}, ], ids=[ "reply", @@ -321,6 +325,9 @@ def message(bot): "external_reply", "quote", "forward_origin", + "reply_to_story", + "boost_added", + "sender_boost_count", ], ) def message_params(bot, request): diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index 7807a02784a..6f83e9ca74e 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -132,6 +132,9 @@ def ptb_extra_params(object_name: str) -> set[str]: # Mostly due to the value being fixed anyway PTB_IGNORED_PARAMS = { r"InlineQueryResult\w+": {"type"}, + # TODO: Remove this in vNEXT.VERSION (API 7.1) when this can stop being optional + r"ChatAdministratorRights": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, + r"ChatMemberAdministrator": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, r"ChatMember\w+": {"status"}, r"PassportElementError\w+": {"source"}, "ForceReply": {"force_reply"}, @@ -166,7 +169,11 @@ def ignored_param_requirements(object_name: str) -> set[str]: # Arguments that are optional arguments for now for backwards compatibility -BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {} +BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = { + # TODO: Remove this in vNEXT.VERSION (API 7.1) when this can stop being optional + r"ChatAdministratorRights": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, + r"ChatMemberAdministrator": {"can_post_stories", "can_edit_stories", "can_delete_stories"}, +} def backwards_compat_kwargs(object_name: str) -> set[str]: diff --git a/tests/test_story.py b/tests/test_story.py index 99524ec7937..b521bebc5c5 100644 --- a/tests/test_story.py +++ b/tests/test_story.py @@ -18,28 +18,55 @@ import pytest -from telegram import Story +from telegram import Chat, Story from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def story(): - return Story() + return Story(TestStoryBase.chat, TestStoryBase.id) -class TestStoryWithoutRequest: - def test_slot_behaviour(self): - story = Story() +class TestStoryBase: + chat = Chat(1, "") + id = 0 + + +class TestStoryWithoutRequest(TestStoryBase): + def test_slot_behaviour(self, story): for attr in story.__slots__: assert getattr(story, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(story)) == len(set(mro_slots(story))), "duplicate slot" - def test_de_json(self): - story = Story.de_json({}, None) + def test_de_json(self, bot): + json_dict = {"chat": self.chat.to_dict(), "id": self.id} + story = Story.de_json(json_dict, bot) assert story.api_kwargs == {} + assert story.chat == self.chat + assert story.id == self.id assert isinstance(story, Story) + assert Story.de_json(None, bot) is None - def test_to_dict(self): - story = Story() + def test_to_dict(self, story): story_dict = story.to_dict() - assert story_dict == {} + assert story_dict["chat"] == self.chat.to_dict() + assert story_dict["id"] == self.id + + def test_equality(self): + a = Story(Chat(1, ""), 0) + b = Story(Chat(1, ""), 0) + c = Story(Chat(1, ""), 1) + d = Story(Chat(2, ""), 0) + e = Chat(1, "") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e)