From 92ff6a8e2bf77dc4225875b54f89af04fe61109d Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sat, 29 May 2021 19:48:16 +0530 Subject: [PATCH] Add __slots__ (#2345) Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> Co-authored-by: Hinrich Mahler --- telegram/base.py | 20 ++- telegram/bot.py | 19 ++ telegram/botcommand.py | 2 + telegram/callbackquery.py | 12 ++ telegram/chat.py | 24 +++ telegram/chataction.py | 5 + telegram/chatinvitelink.py | 10 ++ telegram/chatlocation.py | 2 + telegram/chatmember.py | 26 +++ telegram/chatmemberupdated.py | 10 ++ telegram/chatpermissions.py | 12 ++ telegram/choseninlineresult.py | 2 + telegram/dice.py | 2 + telegram/error.py | 19 ++ telegram/ext/basepersistence.py | 38 +++- telegram/ext/callbackcontext.py | 19 +- telegram/ext/callbackqueryhandler.py | 2 + telegram/ext/chatmemberhandler.py | 1 + telegram/ext/choseninlineresulthandler.py | 2 + telegram/ext/commandhandler.py | 5 + telegram/ext/conversationhandler.py | 23 +++ telegram/ext/defaults.py | 23 ++- telegram/ext/dictpersistence.py | 11 ++ telegram/ext/dispatcher.py | 41 ++++- telegram/ext/filters.py | 166 +++++++++++++++++- telegram/ext/handler.py | 36 +++- telegram/ext/inlinequeryhandler.py | 2 + telegram/ext/jobqueue.py | 20 +++ telegram/ext/messagehandler.py | 2 + telegram/ext/messagequeue.py | 2 + telegram/ext/picklepersistence.py | 10 ++ telegram/ext/pollanswerhandler.py | 2 + telegram/ext/pollhandler.py | 2 + telegram/ext/precheckoutqueryhandler.py | 2 + telegram/ext/regexhandler.py | 2 + telegram/ext/shippingqueryhandler.py | 2 + telegram/ext/stringcommandhandler.py | 2 + telegram/ext/stringregexhandler.py | 2 + telegram/ext/typehandler.py | 6 +- telegram/ext/updater.py | 30 +++- telegram/ext/utils/promise.py | 17 ++ telegram/ext/utils/webhookhandler.py | 16 ++ telegram/files/animation.py | 14 ++ telegram/files/audio.py | 14 ++ telegram/files/chatphoto.py | 9 + telegram/files/contact.py | 2 + telegram/files/document.py | 11 ++ telegram/files/file.py | 10 ++ telegram/files/inputfile.py | 7 + telegram/files/inputmedia.py | 50 ++++++ telegram/files/location.py | 10 ++ telegram/files/photosize.py | 2 + telegram/files/sticker.py | 27 +++ telegram/files/venue.py | 11 ++ telegram/files/video.py | 14 ++ telegram/files/videonote.py | 11 ++ telegram/files/voice.py | 10 ++ telegram/forcereply.py | 2 + telegram/games/callbackgame.py | 2 + telegram/games/game.py | 10 ++ telegram/games/gamehighscore.py | 2 + telegram/inline/inlinekeyboardbutton.py | 12 ++ telegram/inline/inlinekeyboardmarkup.py | 2 + telegram/inline/inlinequery.py | 2 + telegram/inline/inlinequeryresult.py | 2 + telegram/inline/inlinequeryresultarticle.py | 12 ++ telegram/inline/inlinequeryresultaudio.py | 12 ++ .../inline/inlinequeryresultcachedaudio.py | 9 + .../inline/inlinequeryresultcacheddocument.py | 11 ++ telegram/inline/inlinequeryresultcachedgif.py | 10 ++ .../inline/inlinequeryresultcachedmpeg4gif.py | 10 ++ .../inline/inlinequeryresultcachedphoto.py | 11 ++ .../inline/inlinequeryresultcachedsticker.py | 2 + .../inline/inlinequeryresultcachedvideo.py | 11 ++ .../inline/inlinequeryresultcachedvoice.py | 10 ++ telegram/inline/inlinequeryresultcontact.py | 12 ++ telegram/inline/inlinequeryresultdocument.py | 15 ++ telegram/inline/inlinequeryresultgame.py | 2 + telegram/inline/inlinequeryresultgif.py | 15 ++ telegram/inline/inlinequeryresultlocation.py | 15 ++ telegram/inline/inlinequeryresultmpeg4gif.py | 15 ++ telegram/inline/inlinequeryresultphoto.py | 14 ++ telegram/inline/inlinequeryresultvenue.py | 16 ++ telegram/inline/inlinequeryresultvideo.py | 16 ++ telegram/inline/inlinequeryresultvoice.py | 11 ++ telegram/inline/inputcontactmessagecontent.py | 2 + telegram/inline/inputinvoicemessagecontent.py | 24 +++ .../inline/inputlocationmessagecontent.py | 2 + telegram/inline/inputmessagecontent.py | 2 + telegram/inline/inputtextmessagecontent.py | 2 + telegram/inline/inputvenuemessagecontent.py | 12 ++ telegram/keyboardbutton.py | 2 + telegram/keyboardbuttonpolltype.py | 2 + telegram/loginurl.py | 2 + telegram/message.py | 74 +++++++- telegram/messageautodeletetimerchanged.py | 2 + telegram/messageentity.py | 2 + telegram/messageid.py | 2 + telegram/parsemode.py | 6 + telegram/passport/credentials.py | 37 ++++ telegram/passport/data.py | 26 +++ telegram/passport/encryptedpassportelement.py | 15 ++ telegram/passport/passportdata.py | 2 + telegram/passport/passportelementerrors.py | 21 +++ telegram/passport/passportfile.py | 10 ++ telegram/payment/invoice.py | 9 + telegram/payment/labeledprice.py | 2 + telegram/payment/orderinfo.py | 2 + telegram/payment/precheckoutquery.py | 12 ++ telegram/payment/shippingaddress.py | 10 ++ telegram/payment/shippingoption.py | 2 + telegram/payment/shippingquery.py | 2 + telegram/payment/successfulpayment.py | 11 ++ telegram/poll.py | 21 +++ telegram/proximityalerttriggered.py | 2 + telegram/replykeyboardmarkup.py | 2 + telegram/replykeyboardremove.py | 2 + telegram/replymarkup.py | 2 + telegram/update.py | 21 +++ telegram/user.py | 14 ++ telegram/userprofilephotos.py | 2 + telegram/utils/deprecate.py | 19 ++ telegram/utils/helpers.py | 2 + telegram/utils/request.py | 6 + telegram/voicechat.py | 8 + telegram/webhookinfo.py | 12 ++ tests/conftest.py | 30 +++- tests/test_animation.py | 12 +- tests/test_audio.py | 9 + tests/test_bot.py | 26 +++ tests/test_botcommand.py | 8 + tests/test_callbackcontext.py | 10 ++ tests/test_callbackquery.py | 8 + tests/test_callbackqueryhandler.py | 9 + tests/test_chat.py | 8 + tests/test_chataction.py | 29 +++ tests/test_chatinvitelink.py | 8 + tests/test_chatlocation.py | 9 + tests/test_chatmember.py | 8 + tests/test_chatmemberhandler.py | 9 + tests/test_chatmemberupdated.py | 9 + tests/test_chatpermissions.py | 9 + tests/test_chatphoto.py | 9 +- tests/test_choseninlineresult.py | 9 + tests/test_choseninlineresulthandler.py | 9 + tests/test_commandhandler.py | 18 ++ tests/test_contact.py | 8 + tests/test_conversationhandler.py | 11 ++ tests/test_defaults.py | 9 + tests/test_dice.py | 8 + tests/test_dispatcher.py | 20 ++- tests/test_document.py | 9 + tests/test_encryptedcredentials.py | 9 + tests/test_encryptedpassportelement.py | 9 + tests/test_file.py | 8 + tests/test_filters.py | 63 +++++++ tests/test_forcereply.py | 8 + tests/test_game.py | 8 + tests/test_gamehighscore.py | 8 + tests/test_handler.py | 43 +++++ tests/test_inlinekeyboardbutton.py | 9 + tests/test_inlinekeyboardmarkup.py | 9 + tests/test_inlinequery.py | 8 + tests/test_inlinequeryhandler.py | 11 +- tests/test_inlinequeryresultarticle.py | 9 + tests/test_inlinequeryresultaudio.py | 9 + tests/test_inlinequeryresultcachedaudio.py | 9 + tests/test_inlinequeryresultcacheddocument.py | 9 + tests/test_inlinequeryresultcachedgif.py | 10 +- tests/test_inlinequeryresultcachedmpeg4gif.py | 10 +- tests/test_inlinequeryresultcachedphoto.py | 10 +- tests/test_inlinequeryresultcachedsticker.py | 10 +- tests/test_inlinequeryresultcachedvideo.py | 10 +- tests/test_inlinequeryresultcachedvoice.py | 10 +- tests/test_inlinequeryresultcontact.py | 10 +- tests/test_inlinequeryresultdocument.py | 10 +- tests/test_inlinequeryresultgame.py | 10 +- tests/test_inlinequeryresultgif.py | 10 +- tests/test_inlinequeryresultlocation.py | 10 +- tests/test_inlinequeryresultmpeg4gif.py | 10 +- tests/test_inlinequeryresultphoto.py | 10 +- tests/test_inlinequeryresultvenue.py | 10 +- tests/test_inlinequeryresultvideo.py | 10 +- tests/test_inlinequeryresultvoice.py | 10 +- tests/test_inputcontactmessagecontent.py | 10 +- tests/test_inputfile.py | 9 + tests/test_inputinvoicemessagecontent.py | 9 + tests/test_inputlocationmessagecontent.py | 10 +- tests/test_inputmedia.py | 45 +++++ tests/test_inputtextmessagecontent.py | 10 +- tests/test_inputvenuemessagecontent.py | 10 +- tests/test_invoice.py | 9 +- tests/test_jobqueue.py | 8 + tests/test_keyboardbutton.py | 10 +- tests/test_keyboardbuttonpolltype.py | 9 + tests/test_labeledprice.py | 10 +- tests/test_location.py | 9 +- tests/test_loginurl.py | 9 +- tests/test_message.py | 9 +- tests/test_messageautodeletetimerchanged.py | 9 + tests/test_messageentity.py | 10 +- tests/test_messagehandler.py | 9 + tests/test_messageid.py | 9 +- tests/test_messagequeue.py | 3 +- tests/test_orderinfo.py | 9 +- tests/test_parsemode.py | 9 + tests/test_passport.py | 9 + tests/test_passportelementerrordatafield.py | 10 +- tests/test_passportelementerrorfile.py | 10 +- tests/test_passportelementerrorfiles.py | 10 +- tests/test_passportelementerrorfrontside.py | 10 +- tests/test_passportelementerrorreverseside.py | 10 +- tests/test_passportelementerrorselfie.py | 10 +- ...est_passportelementerrortranslationfile.py | 10 +- ...st_passportelementerrortranslationfiles.py | 10 +- tests/test_passportelementerrorunspecified.py | 10 +- tests/test_passportfile.py | 10 +- tests/test_persistence.py | 41 ++++- tests/test_photo.py | 9 + tests/test_poll.py | 9 +- tests/test_pollanswerhandler.py | 9 + tests/test_pollhandler.py | 9 + tests/test_precheckoutquery.py | 10 +- tests/test_precheckoutqueryhandler.py | 9 + tests/test_promise.py | 9 + tests/test_proximityalerttriggered.py | 10 +- tests/test_regexhandler.py | 9 + tests/test_replykeyboardmarkup.py | 10 +- tests/test_replykeyboardremove.py | 10 +- tests/test_request.py | 11 +- tests/test_shippingaddress.py | 10 +- tests/test_shippingoption.py | 10 +- tests/test_shippingquery.py | 10 +- tests/test_shippingqueryhandler.py | 9 + tests/test_slots.py | 58 ++++++ tests/test_sticker.py | 13 ++ tests/test_stringcommandhandler.py | 9 + tests/test_stringregexhandler.py | 9 + tests/test_successfulpayment.py | 10 +- tests/test_telegramobject.py | 11 ++ tests/test_typehandler.py | 9 + tests/test_update.py | 8 + tests/test_updater.py | 19 ++ tests/test_user.py | 8 + tests/test_userprofilephotos.py | 9 + tests/test_venue.py | 9 +- tests/test_video.py | 9 + tests/test_videonote.py | 9 + tests/test_voice.py | 9 + tests/test_voicechat.py | 36 ++++ tests/test_webhookinfo.py | 9 +- 251 files changed, 2908 insertions(+), 99 deletions(-) create mode 100644 tests/test_chataction.py create mode 100644 tests/test_handler.py create mode 100644 tests/test_slots.py diff --git a/telegram/base.py b/telegram/base.py index f33491384..0f906e9a4 100644 --- a/telegram/base.py +++ b/telegram/base.py @@ -26,6 +26,7 @@ import warnings from typing import TYPE_CHECKING, List, Optional, Tuple, Type, TypeVar from telegram.utils.types import JSONDict +from telegram.utils.deprecate import set_new_attribute_deprecated if TYPE_CHECKING: from telegram import Bot @@ -38,11 +39,19 @@ class TelegramObject: _id_attrs: Tuple[object, ...] = () + # Adding slots reduces memory usage & allows for faster attribute access. + # Only instance variables should be added to __slots__. + # We add __dict__ here for backward compatibility & also to avoid repetition for subclasses. + __slots__ = ('__dict__',) + def __str__(self) -> str: return str(self.to_dict()) def __getitem__(self, item: str) -> object: - return self.__dict__[item] + return getattr(self, item, None) + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) @staticmethod def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]: @@ -102,11 +111,16 @@ class TelegramObject: """ data = {} - for key in iter(self.__dict__): + # We want to get all attributes for the class, using self.__slots__ only includes the + # attributes used by that class itself, and not its superclass(es). Hence we get its MRO + # and then get their attributes. The `[:-2]` slice excludes the `object` class & the + # TelegramObject class itself. + attrs = {attr for cls in self.__class__.__mro__[:-2] for attr in cls.__slots__} + for key in attrs: if key == 'bot' or key.startswith('_'): continue - value = self.__dict__[key] + value = getattr(self, key, None) if value is not None: if hasattr(value, 'to_dict'): data[key] = value.to_dict() diff --git a/telegram/bot.py b/telegram/bot.py index 79fcefa2a..797108349 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -158,6 +158,18 @@ class Bot(TelegramObject): """ + __slots__ = ( + 'token', + 'base_url', + 'base_file_url', + 'private_key', + 'defaults', + '_bot', + '_commands', + '_request', + 'logger', + ) + def __init__( self, token: str, @@ -184,6 +196,7 @@ class Bot(TelegramObject): self._bot: Optional[User] = None self._commands: Optional[List[BotCommand]] = None self._request = request or Request() + self.private_key = None self.logger = logging.getLogger(__name__) if private_key: @@ -196,6 +209,12 @@ class Bot(TelegramObject): private_key, password=private_key_password, backend=default_backend() ) + def __setattr__(self, key: str, value: object) -> None: + if issubclass(self.__class__, Bot) and self.__class__ is not Bot: + object.__setattr__(self, key, value) + return + super().__setattr__(key, value) + def _insert_defaults( self, data: Dict[str, object], timeout: ODVInput[float] ) -> Optional[float]: diff --git a/telegram/botcommand.py b/telegram/botcommand.py index 4367370ff..8b36e3e2e 100644 --- a/telegram/botcommand.py +++ b/telegram/botcommand.py @@ -41,6 +41,8 @@ class BotCommand(TelegramObject): """ + __slots__ = ('description', '_id_attrs', 'command') + def __init__(self, command: str, description: str, **_kwargs: Any): self.command = command self.description = description diff --git a/telegram/callbackquery.py b/telegram/callbackquery.py index 370398935..d975d59c9 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -85,6 +85,18 @@ class CallbackQuery(TelegramObject): """ + __slots__ = ( + 'bot', + 'game_short_name', + 'message', + 'chat_instance', + 'id', + 'from_user', + 'inline_message_id', + 'data', + '_id_attrs', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/chat.py b/telegram/chat.py index 6a8b1e0f5..95ed61d5b 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -143,6 +143,30 @@ class Chat(TelegramObject): """ + __slots__ = ( + 'bio', + 'id', + 'type', + 'last_name', + 'bot', + 'sticker_set_name', + 'slow_mode_delay', + 'location', + 'first_name', + 'permissions', + 'invite_link', + 'pinned_message', + 'description', + 'can_set_sticker_set', + 'username', + 'title', + 'photo', + 'linked_chat_id', + 'all_members_are_administrators', + 'message_auto_delete_time', + '_id_attrs', + ) + SENDER: ClassVar[str] = constants.CHAT_SENDER """:const:`telegram.constants.CHAT_SENDER` diff --git a/telegram/chataction.py b/telegram/chataction.py index 43f528bc5..4ecf9a916 100644 --- a/telegram/chataction.py +++ b/telegram/chataction.py @@ -20,11 +20,13 @@ """This module contains an object that represents a Telegram ChatAction.""" from typing import ClassVar from telegram import constants +from telegram.utils.deprecate import set_new_attribute_deprecated class ChatAction: """Helper class to provide constants for different chat actions.""" + __slots__ = ('__dict__',) # Adding __dict__ here since it doesn't subclass TGObject FIND_LOCATION: ClassVar[str] = constants.CHATACTION_FIND_LOCATION """:const:`telegram.constants.CHATACTION_FIND_LOCATION`""" RECORD_AUDIO: ClassVar[str] = constants.CHATACTION_RECORD_AUDIO @@ -65,3 +67,6 @@ class ChatAction: """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO`""" UPLOAD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO_NOTE """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO_NOTE`""" + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) diff --git a/telegram/chatinvitelink.py b/telegram/chatinvitelink.py index ecffe50cc..0755853b0 100644 --- a/telegram/chatinvitelink.py +++ b/telegram/chatinvitelink.py @@ -60,6 +60,16 @@ class ChatInviteLink(TelegramObject): """ + __slots__ = ( + 'invite_link', + 'creator', + 'is_primary', + 'is_revoked', + 'expire_date', + 'member_limit', + '_id_attrs', + ) + def __init__( self, invite_link: str, diff --git a/telegram/chatlocation.py b/telegram/chatlocation.py index 3d74fa5f9..dcdbb6f00 100644 --- a/telegram/chatlocation.py +++ b/telegram/chatlocation.py @@ -47,6 +47,8 @@ class ChatLocation(TelegramObject): """ + __slots__ = ('location', '_id_attrs', 'address') + def __init__( self, location: Location, diff --git a/telegram/chatmember.py b/telegram/chatmember.py index 722281f39..3246c4b91 100644 --- a/telegram/chatmember.py +++ b/telegram/chatmember.py @@ -138,6 +138,32 @@ class ChatMember(TelegramObject): """ + __slots__ = ( + 'is_member', + 'can_restrict_members', + 'can_delete_messages', + 'custom_title', + 'can_be_edited', + 'can_post_messages', + 'can_send_messages', + 'can_edit_messages', + 'can_send_media_messages', + 'is_anonymous', + 'can_add_web_page_previews', + 'can_send_other_messages', + 'can_invite_users', + 'can_send_polls', + 'user', + 'can_promote_members', + 'status', + 'can_change_info', + 'can_pin_messages', + 'can_manage_chat', + 'can_manage_voice_chats', + 'until_date', + '_id_attrs', + ) + ADMINISTRATOR: ClassVar[str] = constants.CHATMEMBER_ADMINISTRATOR """:const:`telegram.constants.CHATMEMBER_ADMINISTRATOR`""" CREATOR: ClassVar[str] = constants.CHATMEMBER_CREATOR diff --git a/telegram/chatmemberupdated.py b/telegram/chatmemberupdated.py index 4304beaf9..4d49a6c7e 100644 --- a/telegram/chatmemberupdated.py +++ b/telegram/chatmemberupdated.py @@ -62,6 +62,16 @@ class ChatMemberUpdated(TelegramObject): """ + __slots__ = ( + 'chat', + 'from_user', + 'date', + 'old_chat_member', + 'new_chat_member', + 'invite_link', + '_id_attrs', + ) + def __init__( self, chat: Chat, diff --git a/telegram/chatpermissions.py b/telegram/chatpermissions.py index fec9d9d1c..0b5a7b956 100644 --- a/telegram/chatpermissions.py +++ b/telegram/chatpermissions.py @@ -78,6 +78,18 @@ class ChatPermissions(TelegramObject): """ + __slots__ = ( + 'can_send_other_messages', + 'can_invite_users', + 'can_send_polls', + '_id_attrs', + 'can_send_messages', + 'can_send_media_messages', + 'can_change_info', + 'can_pin_messages', + 'can_add_web_page_previews', + ) + def __init__( self, can_send_messages: bool = None, diff --git a/telegram/choseninlineresult.py b/telegram/choseninlineresult.py index a7f5cfd01..384d57e63 100644 --- a/telegram/choseninlineresult.py +++ b/telegram/choseninlineresult.py @@ -61,6 +61,8 @@ class ChosenInlineResult(TelegramObject): """ + __slots__ = ('location', 'result_id', 'from_user', 'inline_message_id', '_id_attrs', 'query') + def __init__( self, result_id: str, diff --git a/telegram/dice.py b/telegram/dice.py index f3715f007..3406ceeda 100644 --- a/telegram/dice.py +++ b/telegram/dice.py @@ -64,6 +64,8 @@ class Dice(TelegramObject): """ + __slots__ = ('emoji', 'value', '_id_attrs') + def __init__(self, value: int, emoji: str, **_kwargs: Any): self.value = value self.emoji = emoji diff --git a/telegram/error.py b/telegram/error.py index 6a546c449..5e597cd2b 100644 --- a/telegram/error.py +++ b/telegram/error.py @@ -41,6 +41,9 @@ def _lstrip_str(in_s: str, lstr: str) -> str: class TelegramError(Exception): """Base class for Telegram errors.""" + # Apparently the base class Exception already has __dict__ in it, so its not included here + __slots__ = ('message',) + def __init__(self, message: str): super().__init__() @@ -62,10 +65,14 @@ class TelegramError(Exception): class Unauthorized(TelegramError): """Raised when the bot has not enough rights to perform the requested action.""" + __slots__ = () + class InvalidToken(TelegramError): """Raised when the token is invalid.""" + __slots__ = () + def __init__(self) -> None: super().__init__('Invalid token') @@ -76,14 +83,20 @@ class InvalidToken(TelegramError): class NetworkError(TelegramError): """Base class for exceptions due to networking errors.""" + __slots__ = () + class BadRequest(NetworkError): """Raised when Telegram could not process the request correctly.""" + __slots__ = () + class TimedOut(NetworkError): """Raised when a request took too long to finish.""" + __slots__ = () + def __init__(self) -> None: super().__init__('Timed out') @@ -100,6 +113,8 @@ class ChatMigrated(TelegramError): """ + __slots__ = ('new_chat_id',) + def __init__(self, new_chat_id: int): super().__init__(f'Group migrated to supergroup. New chat id: {new_chat_id}') self.new_chat_id = new_chat_id @@ -117,6 +132,8 @@ class RetryAfter(TelegramError): """ + __slots__ = ('retry_after',) + def __init__(self, retry_after: int): super().__init__(f'Flood control exceeded. Retry in {float(retry_after)} seconds') self.retry_after = float(retry_after) @@ -128,5 +145,7 @@ class RetryAfter(TelegramError): class Conflict(TelegramError): """Raised when a long poll or webhook conflicts with another one.""" + __slots__ = () + def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self.message,) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 8172d8eed..c0c248e5c 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -18,10 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the BasePersistence class.""" import warnings +from sys import version_info as py_ver from abc import ABC, abstractmethod from copy import copy from typing import DefaultDict, Dict, Optional, Tuple, cast, ClassVar +from telegram.utils.deprecate import set_new_attribute_deprecated + from telegram import Bot from telegram.utils.types import ConversationDict @@ -75,6 +78,18 @@ class BasePersistence(ABC): persistence class. """ + # Apparently Py 3.7 and below have '__dict__' in ABC + if py_ver < (3, 7): + __slots__ = ('store_user_data', 'store_chat_data', 'store_bot_data', 'bot') + else: + __slots__ = ( + 'store_user_data', # type: ignore[assignment] + 'store_chat_data', + 'store_bot_data', + 'bot', + '__dict__', + ) + def __new__( cls, *args: object, **kwargs: object # pylint: disable=W0613 ) -> 'BasePersistence': @@ -104,12 +119,13 @@ class BasePersistence(ABC): def update_bot_data_replace_bot(data: Dict) -> None: return update_bot_data(instance.replace_bot(data)) - instance.get_user_data = get_user_data_insert_bot - instance.get_chat_data = get_chat_data_insert_bot - instance.get_bot_data = get_bot_data_insert_bot - instance.update_user_data = update_user_data_replace_bot - instance.update_chat_data = update_chat_data_replace_bot - instance.update_bot_data = update_bot_data_replace_bot + # We want to ignore TGDeprecation warnings so we use obj.__setattr__. Adds to __dict__ + object.__setattr__(instance, 'get_user_data', get_user_data_insert_bot) + object.__setattr__(instance, 'get_chat_data', get_chat_data_insert_bot) + object.__setattr__(instance, 'get_bot_data', get_bot_data_insert_bot) + object.__setattr__(instance, 'update_user_data', update_user_data_replace_bot) + object.__setattr__(instance, 'update_chat_data', update_chat_data_replace_bot) + object.__setattr__(instance, 'update_bot_data', update_bot_data_replace_bot) return instance def __init__( @@ -123,6 +139,16 @@ class BasePersistence(ABC): self.store_bot_data = store_bot_data self.bot: Bot = None # type: ignore[assignment] + def __setattr__(self, key: str, value: object) -> None: + # Allow user defined subclasses to have custom attributes. + if issubclass(self.__class__, BasePersistence) and self.__class__.__name__ not in { + 'DictPersistence', + 'PicklePersistence', + }: + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + def set_bot(self, bot: Bot) -> None: """Set the Bot to be used by this persistence instance. diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index 7f84016a6..d27840b2e 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -73,6 +73,20 @@ class CallbackContext: """ + __slots__ = ( + '_dispatcher', + '_bot_data', + '_chat_data', + '_user_data', + 'args', + 'matches', + 'error', + 'job', + 'async_args', + 'async_kwargs', + '__dict__', + ) + def __init__(self, dispatcher: 'Dispatcher'): """ Args: @@ -229,12 +243,13 @@ class CallbackContext: return self def update(self, data: Dict[str, object]) -> None: - """Updates ``self.__dict__`` with the passed data. + """Updates ``self.__slots__`` with the passed data. Args: data (Dict[:obj:`str`, :obj:`object`]): The data. """ - self.__dict__.update(data) + for key, value in data.items(): + setattr(self, key, value) @property def bot(self) -> 'Bot': diff --git a/telegram/ext/callbackqueryhandler.py b/telegram/ext/callbackqueryhandler.py index df0205359..95eb8afb1 100644 --- a/telegram/ext/callbackqueryhandler.py +++ b/telegram/ext/callbackqueryhandler.py @@ -120,6 +120,8 @@ class CallbackQueryHandler(Handler[Update]): """ + __slots__ = ('pattern', 'pass_groups', 'pass_groupdict') + def __init__( self, callback: Callable[[Update, 'CallbackContext'], RT], diff --git a/telegram/ext/chatmemberhandler.py b/telegram/ext/chatmemberhandler.py index 35ce49e2a..c098db904 100644 --- a/telegram/ext/chatmemberhandler.py +++ b/telegram/ext/chatmemberhandler.py @@ -96,6 +96,7 @@ class ChatMemberHandler(Handler[Update]): """ + __slots__ = ('chat_member_types',) MY_CHAT_MEMBER: ClassVar[int] = -1 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`.""" CHAT_MEMBER: ClassVar[int] = 0 diff --git a/telegram/ext/choseninlineresulthandler.py b/telegram/ext/choseninlineresulthandler.py index 6e2b59b1f..2ae2814f6 100644 --- a/telegram/ext/choseninlineresulthandler.py +++ b/telegram/ext/choseninlineresulthandler.py @@ -99,6 +99,8 @@ class ChosenInlineResultHandler(Handler[Update]): """ + __slots__ = ('pattern',) + def __init__( self, callback: Callable[[Update, 'CallbackContext'], RT], diff --git a/telegram/ext/commandhandler.py b/telegram/ext/commandhandler.py index f5ab7c4d9..6d6a1b6e6 100644 --- a/telegram/ext/commandhandler.py +++ b/telegram/ext/commandhandler.py @@ -129,6 +129,8 @@ class CommandHandler(Handler[Update]): run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ + __slots__ = ('command', 'filters', 'pass_args') + def __init__( self, command: SLT[str], @@ -350,6 +352,9 @@ class PrefixHandler(CommandHandler): """ + # 'prefix' is a class property, & 'command' is included in the superclass, so they're left out. + __slots__ = ('_prefix', '_command', '_commands') + def __init__( self, prefix: SLT[str], diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py index d4739315a..ffaaf969c 100644 --- a/telegram/ext/conversationhandler.py +++ b/telegram/ext/conversationhandler.py @@ -45,6 +45,9 @@ CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]] class _ConversationTimeoutContext: + # '__dict__' is not included since this a private class + __slots__ = ('conversation_key', 'update', 'dispatcher', 'callback_context') + def __init__( self, conversation_key: Tuple[int, ...], @@ -182,6 +185,26 @@ class ConversationHandler(Handler[Update]): """ + __slots__ = ( + '_entry_points', + '_states', + '_fallbacks', + '_allow_reentry', + '_per_user', + '_per_chat', + '_per_message', + '_conversation_timeout', + '_name', + 'persistent', + '_persistence', + '_map_to_parent', + 'timeout_jobs', + '_timeout_jobs_lock', + '_conversations', + '_conversations_lock', + 'logger', + ) + END: ClassVar[int] = -1 """:obj:`int`: Used as a constant to return when a conversation is ended.""" TIMEOUT: ClassVar[int] = -2 diff --git a/telegram/ext/defaults.py b/telegram/ext/defaults.py index c9d66a3a8..d2f8e0679 100644 --- a/telegram/ext/defaults.py +++ b/telegram/ext/defaults.py @@ -22,6 +22,7 @@ from typing import NoReturn, Optional, Dict, Any import pytz +from telegram.utils.deprecate import set_new_attribute_deprecated from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput @@ -56,6 +57,19 @@ class Defaults: :meth:`Dispatcher.add_error_handler`. Defaults to :obj:`False`. """ + __slots__ = ( + '_timeout', + '_tzinfo', + '_disable_web_page_preview', + '_run_async', + '_quote', + '_disable_notification', + '_allow_sending_without_reply', + '_parse_mode', + '_api_defaults', + '__dict__', + ) + def __init__( self, parse_mode: str = None, @@ -91,8 +105,11 @@ class Defaults: if value not in [None, DEFAULT_NONE]: self._api_defaults[kwarg] = value # Special casing, as None is a valid default value - if self.timeout != DEFAULT_NONE: - self._api_defaults['timeout'] = self.timeout + if self._timeout != DEFAULT_NONE: + self._api_defaults['timeout'] = self._timeout + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) @property def api_defaults(self) -> Dict[str, Any]: # skip-cq: PY-D0003 @@ -243,7 +260,7 @@ class Defaults: def __eq__(self, other: object) -> bool: if isinstance(other, Defaults): - return self.__dict__ == other.__dict__ + return all(getattr(self, attr) == getattr(other, attr) for attr in self.__slots__) return False def __ne__(self, other: object) -> bool: diff --git a/telegram/ext/dictpersistence.py b/telegram/ext/dictpersistence.py index 572a27e85..e5df61605 100644 --- a/telegram/ext/dictpersistence.py +++ b/telegram/ext/dictpersistence.py @@ -79,6 +79,17 @@ class DictPersistence(BasePersistence): persistence class. """ + __slots__ = ( + '_user_data', + '_chat_data', + '_bot_data', + '_conversations', + '_user_data_json', + '_chat_data_json', + '_bot_data_json', + '_conversations_json', + ) + def __init__( self, store_user_data: bool = True, diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 9ca87046b..4dbd2382d 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -26,14 +26,14 @@ from functools import wraps from queue import Empty, Queue from threading import BoundedSemaphore, Event, Lock, Thread, current_thread from time import sleep -from typing import TYPE_CHECKING, Callable, DefaultDict, Dict, List, Optional, Set, Union +from typing import TYPE_CHECKING, Callable, DefaultDict, Dict, List, Optional, Union, Set from uuid import uuid4 from telegram import TelegramError, Update from telegram.ext import BasePersistence from telegram.ext.callbackcontext import CallbackContext from telegram.ext.handler import Handler -from telegram.utils.deprecate import TelegramDeprecationWarning +from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated from telegram.ext.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE @@ -98,6 +98,8 @@ class DispatcherHandlerStop(Exception): state (:obj:`object`, optional): The next state of the conversation. """ + __slots__ = ('state',) + def __init__(self, state: object = None) -> None: super().__init__() self.state = state @@ -134,6 +136,30 @@ class Dispatcher: """ + # Allowing '__weakref__' creation here since we need it for the singleton + __slots__ = ( + 'workers', + 'persistence', + 'use_context', + 'update_queue', + 'job_queue', + 'user_data', + 'chat_data', + 'bot_data', + '_update_persistence_lock', + 'handlers', + 'groups', + 'error_handlers', + 'running', + '__stop_event', + '__exception_event', + '__async_queue', + '__async_threads', + 'bot', + '__dict__', + '__weakref__', + ) + __singleton_lock = Lock() __singleton_semaphore = BoundedSemaphore() __singleton = None @@ -215,6 +241,17 @@ class Dispatcher: else: self._set_singleton(None) + def __setattr__(self, key: str, value: object) -> None: + # Mangled names don't automatically apply in __setattr__ (see + # https://docs.python.org/3/tutorial/classes.html#private-variables), so we have to make + # it mangled so they don't raise TelegramDeprecationWarning unnecessarily + if key.startswith('__'): + key = f"_{self.__class__.__name__}{key}" + if issubclass(self.__class__, Dispatcher) and self.__class__ is not Dispatcher: + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + @property def exception_event(self) -> Event: # skipcq: PY-D0003 return self.__exception_event diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 27e788da4..15610d273 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -23,6 +23,7 @@ import re import warnings from abc import ABC, abstractmethod +from sys import version_info as py_ver from threading import Lock from typing import ( Dict, @@ -50,7 +51,7 @@ __all__ = [ 'XORFilter', ] -from telegram.utils.deprecate import TelegramDeprecationWarning +from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated from telegram.utils.types import SLT DataDict = Dict[str, list] @@ -112,8 +113,17 @@ class BaseFilter(ABC): (depends on the handler). """ - _name = None - data_filter = False + if py_ver < (3, 7): + __slots__ = ('_name', '_data_filter') + else: + __slots__ = ('_name', '_data_filter', '__dict__') # type: ignore[assignment] + + def __new__(cls, *args: object, **kwargs: object) -> 'BaseFilter': # pylint: disable=W0613 + instance = super().__new__(cls) + instance._name = None + instance._data_filter = False + + return instance @abstractmethod def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: @@ -131,13 +141,33 @@ class BaseFilter(ABC): def __invert__(self) -> 'BaseFilter': return InvertedFilter(self) + def __setattr__(self, key: str, value: object) -> None: + # Allow setting custom attributes w/o warning for user defined custom filters. + # To differentiate between a custom and a PTB filter, we use this hacky but + # simple way of checking the module name where the class is defined from. + if ( + issubclass(self.__class__, (UpdateFilter, MessageFilter)) + and self.__class__.__module__ != __name__ + ): # __name__ is telegram.ext.filters + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + + @property + def data_filter(self) -> bool: + return self._data_filter + + @data_filter.setter + def data_filter(self, value: bool) -> None: + self._data_filter = value + @property def name(self) -> Optional[str]: return self._name @name.setter def name(self, name: Optional[str]) -> None: - self._name = name + self._name = name # pylint: disable=E0237 def __repr__(self) -> str: # We do this here instead of in a __init__ so filter don't have to call __init__ or super() @@ -146,7 +176,7 @@ class BaseFilter(ABC): return self.name -class MessageFilter(BaseFilter, ABC): +class MessageFilter(BaseFilter): """Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed to :meth:`filter` is ``update.effective_message``. @@ -162,6 +192,8 @@ class MessageFilter(BaseFilter, ABC): """ + __slots__ = () + def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: return self.filter(update.effective_message) @@ -178,7 +210,7 @@ class MessageFilter(BaseFilter, ABC): """ -class UpdateFilter(BaseFilter, ABC): +class UpdateFilter(BaseFilter): """Base class for all Update Filters. In contrast to :class:`MessageFilter`, the object passed to :meth:`filter` is ``update``, which allows to create filters like :attr:`Filters.update.edited_message`. @@ -195,6 +227,8 @@ class UpdateFilter(BaseFilter, ABC): """ + __slots__ = () + def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: return self.filter(update) @@ -219,6 +253,8 @@ class InvertedFilter(UpdateFilter): """ + __slots__ = ('f',) + def __init__(self, f: BaseFilter): self.f = f @@ -244,6 +280,8 @@ class MergedFilter(UpdateFilter): """ + __slots__ = ('base_filter', 'and_filter', 'or_filter') + def __init__( self, base_filter: BaseFilter, and_filter: BaseFilter = None, or_filter: BaseFilter = None ): @@ -328,6 +366,8 @@ class XORFilter(UpdateFilter): """ + __slots__ = ('base_filter', 'xor_filter', 'merged_filter') + def __init__(self, base_filter: BaseFilter, xor_filter: BaseFilter): self.base_filter = base_filter self.xor_filter = xor_filter @@ -346,11 +386,15 @@ class XORFilter(UpdateFilter): class _DiceEmoji(MessageFilter): + __slots__ = ('emoji',) + def __init__(self, emoji: str = None, name: str = None): self.name = f'Filters.dice.{name}' if name else 'Filters.dice' self.emoji = emoji class _DiceValues(MessageFilter): + __slots__ = ('values', 'emoji') + def __init__( self, values: SLT[int], @@ -393,7 +437,13 @@ class Filters: """ + __slots__ = ('__dict__',) + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + class _All(MessageFilter): + __slots__ = () name = 'Filters.all' def filter(self, message: Message) -> bool: @@ -403,9 +453,12 @@ class Filters: """All Messages.""" class _Text(MessageFilter): + __slots__ = () name = 'Filters.text' class _TextStrings(MessageFilter): + __slots__ = ('strings',) + def __init__(self, strings: Union[List[str], Tuple[str]]): self.strings = strings self.name = f'Filters.text({strings})' @@ -454,9 +507,12 @@ class Filters: """ class _Caption(MessageFilter): + __slots__ = () name = 'Filters.caption' class _CaptionStrings(MessageFilter): + __slots__ = ('strings',) + def __init__(self, strings: Union[List[str], Tuple[str]]): self.strings = strings self.name = f'Filters.caption({strings})' @@ -489,9 +545,12 @@ class Filters: """ class _Command(MessageFilter): + __slots__ = () name = 'Filters.command' class _CommandOnlyStart(MessageFilter): + __slots__ = ('only_start',) + def __init__(self, only_start: bool): self.only_start = only_start self.name = f'Filters.command({only_start})' @@ -564,6 +623,7 @@ class Filters: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. """ + __slots__ = ('pattern',) data_filter = True def __init__(self, pattern: Union[str, Pattern]): @@ -599,6 +659,7 @@ class Filters: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. """ + __slots__ = ('pattern',) data_filter = True def __init__(self, pattern: Union[str, Pattern]): @@ -617,6 +678,7 @@ class Filters: return {} class _Reply(MessageFilter): + __slots__ = () name = 'Filters.reply' def filter(self, message: Message) -> bool: @@ -626,6 +688,7 @@ class Filters: """Messages that are a reply to another message.""" class _Audio(MessageFilter): + __slots__ = () name = 'Filters.audio' def filter(self, message: Message) -> bool: @@ -635,6 +698,7 @@ class Filters: """Messages that contain :class:`telegram.Audio`.""" class _Document(MessageFilter): + __slots__ = () name = 'Filters.document' class category(MessageFilter): @@ -651,7 +715,14 @@ class Filters: of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. """ + __slots__ = ('_category',) + def __init__(self, category: Optional[str]): + """Initialize the category you want to filter + + Args: + category (str, optional): category of the media you want to filter + """ self._category = category self.name = f"Filters.document.category('{self._category}')" @@ -680,6 +751,8 @@ class Filters: ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format. """ + __slots__ = ('mimetype',) + def __init__(self, mimetype: Optional[str]): self.mimetype = mimetype self.name = f"Filters.document.mime_type('{self.mimetype}')" @@ -732,6 +805,8 @@ class Filters: filters files without a dot in the filename. """ + __slots__ = ('_file_extension', 'is_case_sensitive') + def __init__(self, file_extension: Optional[str], case_sensitive: bool = False): """Initialize the extension you want to filter. @@ -850,6 +925,7 @@ officedocument.wordprocessingml.document")``. """ class _Animation(MessageFilter): + __slots__ = () name = 'Filters.animation' def filter(self, message: Message) -> bool: @@ -859,6 +935,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.Animation`.""" class _Photo(MessageFilter): + __slots__ = () name = 'Filters.photo' def filter(self, message: Message) -> bool: @@ -868,6 +945,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.PhotoSize`.""" class _Sticker(MessageFilter): + __slots__ = () name = 'Filters.sticker' def filter(self, message: Message) -> bool: @@ -877,6 +955,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.Sticker`.""" class _Video(MessageFilter): + __slots__ = () name = 'Filters.video' def filter(self, message: Message) -> bool: @@ -886,6 +965,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.Video`.""" class _Voice(MessageFilter): + __slots__ = () name = 'Filters.voice' def filter(self, message: Message) -> bool: @@ -895,6 +975,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.Voice`.""" class _VideoNote(MessageFilter): + __slots__ = () name = 'Filters.video_note' def filter(self, message: Message) -> bool: @@ -904,6 +985,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.VideoNote`.""" class _Contact(MessageFilter): + __slots__ = () name = 'Filters.contact' def filter(self, message: Message) -> bool: @@ -913,6 +995,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.Contact`.""" class _Location(MessageFilter): + __slots__ = () name = 'Filters.location' def filter(self, message: Message) -> bool: @@ -922,6 +1005,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.Location`.""" class _Venue(MessageFilter): + __slots__ = () name = 'Filters.venue' def filter(self, message: Message) -> bool: @@ -939,7 +1023,10 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = () + class _NewChatMembers(MessageFilter): + __slots__ = () name = 'Filters.status_update.new_chat_members' def filter(self, message: Message) -> bool: @@ -949,6 +1036,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.new_chat_members`.""" class _LeftChatMember(MessageFilter): + __slots__ = () name = 'Filters.status_update.left_chat_member' def filter(self, message: Message) -> bool: @@ -958,6 +1046,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.left_chat_member`.""" class _NewChatTitle(MessageFilter): + __slots__ = () name = 'Filters.status_update.new_chat_title' def filter(self, message: Message) -> bool: @@ -967,6 +1056,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.new_chat_title`.""" class _NewChatPhoto(MessageFilter): + __slots__ = () name = 'Filters.status_update.new_chat_photo' def filter(self, message: Message) -> bool: @@ -976,6 +1066,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.new_chat_photo`.""" class _DeleteChatPhoto(MessageFilter): + __slots__ = () name = 'Filters.status_update.delete_chat_photo' def filter(self, message: Message) -> bool: @@ -985,6 +1076,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" class _ChatCreated(MessageFilter): + __slots__ = () name = 'Filters.status_update.chat_created' def filter(self, message: Message) -> bool: @@ -1000,6 +1092,7 @@ officedocument.wordprocessingml.document")``. :attr: `telegram.Message.channel_chat_created`.""" class _MessageAutoDeleteTimerChanged(MessageFilter): + __slots__ = () name = 'MessageAutoDeleteTimerChanged' def filter(self, message: Message) -> bool: @@ -1009,6 +1102,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`message_auto_delete_timer_changed`""" class _Migrate(MessageFilter): + __slots__ = () name = 'Filters.status_update.migrate' def filter(self, message: Message) -> bool: @@ -1019,6 +1113,7 @@ officedocument.wordprocessingml.document")``. :attr:`telegram.Message.migrate_to_chat_id`.""" class _PinnedMessage(MessageFilter): + __slots__ = () name = 'Filters.status_update.pinned_message' def filter(self, message: Message) -> bool: @@ -1028,6 +1123,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.pinned_message`.""" class _ConnectedWebsite(MessageFilter): + __slots__ = () name = 'Filters.status_update.connected_website' def filter(self, message: Message) -> bool: @@ -1037,6 +1133,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.connected_website`.""" class _ProximityAlertTriggered(MessageFilter): + __slots__ = () name = 'Filters.status_update.proximity_alert_triggered' def filter(self, message: Message) -> bool: @@ -1046,6 +1143,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" class _VoiceChatScheduled(MessageFilter): + __slots__ = () name = 'Filters.status_update.voice_chat_scheduled' def filter(self, message: Message) -> bool: @@ -1055,6 +1153,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.voice_chat_scheduled`.""" class _VoiceChatStarted(MessageFilter): + __slots__ = () name = 'Filters.status_update.voice_chat_started' def filter(self, message: Message) -> bool: @@ -1064,6 +1163,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.voice_chat_started`.""" class _VoiceChatEnded(MessageFilter): + __slots__ = () name = 'Filters.status_update.voice_chat_ended' def filter(self, message: Message) -> bool: @@ -1073,6 +1173,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :attr:`telegram.Message.voice_chat_ended`.""" class _VoiceChatParticipantsInvited(MessageFilter): + __slots__ = () name = 'Filters.status_update.voice_chat_participants_invited' def filter(self, message: Message) -> bool: @@ -1157,6 +1258,7 @@ officedocument.wordprocessingml.document")``. """ class _Forwarded(MessageFilter): + __slots__ = () name = 'Filters.forwarded' def filter(self, message: Message) -> bool: @@ -1166,6 +1268,7 @@ officedocument.wordprocessingml.document")``. """Messages that are forwarded.""" class _Game(MessageFilter): + __slots__ = () name = 'Filters.game' def filter(self, message: Message) -> bool: @@ -1188,6 +1291,8 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = ('entity_type',) + def __init__(self, entity_type: str): self.entity_type = entity_type self.name = f'Filters.entity({self.entity_type})' @@ -1210,6 +1315,8 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = ('entity_type',) + def __init__(self, entity_type: str): self.entity_type = entity_type self.name = f'Filters.caption_entity({self.entity_type})' @@ -1219,6 +1326,7 @@ officedocument.wordprocessingml.document")``. return any(entity.type == self.entity_type for entity in message.caption_entities) class _Private(MessageFilter): + __slots__ = () name = 'Filters.private' def filter(self, message: Message) -> bool: @@ -1239,6 +1347,7 @@ officedocument.wordprocessingml.document")``. """ class _Group(MessageFilter): + __slots__ = () name = 'Filters.group' def filter(self, message: Message) -> bool: @@ -1259,9 +1368,11 @@ officedocument.wordprocessingml.document")``. """ class _ChatType(MessageFilter): + __slots__ = () name = 'Filters.chat_type' class _Channel(MessageFilter): + __slots__ = () name = 'Filters.chat_type.channel' def filter(self, message: Message) -> bool: @@ -1270,6 +1381,7 @@ officedocument.wordprocessingml.document")``. channel = _Channel() class _Group(MessageFilter): + __slots__ = () name = 'Filters.chat_type.group' def filter(self, message: Message) -> bool: @@ -1278,6 +1390,7 @@ officedocument.wordprocessingml.document")``. group = _Group() class _SuperGroup(MessageFilter): + __slots__ = () name = 'Filters.chat_type.supergroup' def filter(self, message: Message) -> bool: @@ -1286,6 +1399,7 @@ officedocument.wordprocessingml.document")``. supergroup = _SuperGroup() class _Groups(MessageFilter): + __slots__ = () name = 'Filters.chat_type.groups' def filter(self, message: Message) -> bool: @@ -1294,6 +1408,7 @@ officedocument.wordprocessingml.document")``. groups = _Groups() class _Private(MessageFilter): + __slots__ = () name = 'Filters.chat_type.private' def filter(self, message: Message) -> bool: @@ -1321,6 +1436,15 @@ officedocument.wordprocessingml.document")``. """ class _ChatUserBaseFilter(MessageFilter, ABC): + __slots__ = ( + 'chat_id_name', + 'username_name', + 'allow_empty', + '__lock', + '_chat_ids', + '_usernames', + ) + def __init__( self, chat_id: SLT[int] = None, @@ -1497,6 +1621,8 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = () + def __init__( self, user_id: SLT[int] = None, @@ -1596,6 +1722,8 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = () + def __init__( self, bot_id: SLT[int] = None, @@ -1695,6 +1823,8 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = () + def get_chat_or_user(self, message: Message) -> Optional[Chat]: return message.chat @@ -1786,6 +1916,8 @@ officedocument.wordprocessingml.document")``. is specified in :attr:`chat_ids` and :attr:`usernames`. """ + __slots__ = () + def get_chat_or_user(self, message: Message) -> Union[User, Chat, None]: return message.forward_from or message.forward_from_chat @@ -1891,6 +2023,8 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = () + def get_chat_or_user(self, message: Message) -> Optional[Chat]: return message.sender_chat @@ -1937,12 +2071,16 @@ officedocument.wordprocessingml.document")``. return super().remove_chat_ids(chat_id) class _SuperGroup(MessageFilter): + __slots__ = () + def filter(self, message: Message) -> bool: if message.sender_chat: return message.sender_chat.type == Chat.SUPERGROUP return False class _Channel(MessageFilter): + __slots__ = () + def filter(self, message: Message) -> bool: if message.sender_chat: return message.sender_chat.type == Chat.CHANNEL @@ -1952,6 +2090,7 @@ officedocument.wordprocessingml.document")``. channel = _Channel() class _Invoice(MessageFilter): + __slots__ = () name = 'Filters.invoice' def filter(self, message: Message) -> bool: @@ -1961,6 +2100,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain :class:`telegram.Invoice`.""" class _SuccessfulPayment(MessageFilter): + __slots__ = () name = 'Filters.successful_payment' def filter(self, message: Message) -> bool: @@ -1970,6 +2110,7 @@ officedocument.wordprocessingml.document")``. """Messages that confirm a :class:`telegram.SuccessfulPayment`.""" class _PassportData(MessageFilter): + __slots__ = () name = 'Filters.passport_data' def filter(self, message: Message) -> bool: @@ -1979,6 +2120,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain a :class:`telegram.PassportData`""" class _Poll(MessageFilter): + __slots__ = () name = 'Filters.poll' def filter(self, message: Message) -> bool: @@ -1988,6 +2130,7 @@ officedocument.wordprocessingml.document")``. """Messages that contain a :class:`telegram.Poll`.""" class _Dice(_DiceEmoji): + __slots__ = () dice = _DiceEmoji('🎲', 'dice') darts = _DiceEmoji('🎯', 'darts') basketball = _DiceEmoji('🏀', 'basketball') @@ -2051,6 +2194,8 @@ officedocument.wordprocessingml.document")``. """ + __slots__ = ('lang',) + def __init__(self, lang: SLT[str]): if isinstance(lang, str): lang = cast(str, lang) @@ -2068,6 +2213,8 @@ officedocument.wordprocessingml.document")``. ) class _Attachment(MessageFilter): + __slots__ = () + name = 'Filters.attachment' def filter(self, message: Message) -> bool: @@ -2080,9 +2227,11 @@ officedocument.wordprocessingml.document")``. .. versionadded:: 13.6""" class _UpdateType(UpdateFilter): + __slots__ = () name = 'Filters.update' class _Message(UpdateFilter): + __slots__ = () name = 'Filters.update.message' def filter(self, update: Update) -> bool: @@ -2091,6 +2240,7 @@ officedocument.wordprocessingml.document")``. message = _Message() class _EditedMessage(UpdateFilter): + __slots__ = () name = 'Filters.update.edited_message' def filter(self, update: Update) -> bool: @@ -2099,6 +2249,7 @@ officedocument.wordprocessingml.document")``. edited_message = _EditedMessage() class _Messages(UpdateFilter): + __slots__ = () name = 'Filters.update.messages' def filter(self, update: Update) -> bool: @@ -2107,6 +2258,7 @@ officedocument.wordprocessingml.document")``. messages = _Messages() class _ChannelPost(UpdateFilter): + __slots__ = () name = 'Filters.update.channel_post' def filter(self, update: Update) -> bool: @@ -2115,6 +2267,7 @@ officedocument.wordprocessingml.document")``. channel_post = _ChannelPost() class _EditedChannelPost(UpdateFilter): + __slots__ = () name = 'Filters.update.edited_channel_post' def filter(self, update: Update) -> bool: @@ -2123,6 +2276,7 @@ officedocument.wordprocessingml.document")``. edited_channel_post = _EditedChannelPost() class _ChannelPosts(UpdateFilter): + __slots__ = () name = 'Filters.update.channel_posts' def filter(self, update: Update) -> bool: diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index c2fbce33b..17a86d166 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -17,9 +17,11 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the base class for handlers as used by the Dispatcher.""" - from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, Union, Generic +from sys import version_info as py_ver + +from telegram.utils.deprecate import set_new_attribute_deprecated from telegram import Update from telegram.ext.utils.promise import Promise @@ -90,6 +92,27 @@ class Handler(Generic[UT], ABC): """ + # Apparently Py 3.7 and below have '__dict__' in ABC + if py_ver < (3, 7): + __slots__ = ( + 'callback', + 'pass_update_queue', + 'pass_job_queue', + 'pass_user_data', + 'pass_chat_data', + 'run_async', + ) + else: + __slots__ = ( + 'callback', # type: ignore[assignment] + 'pass_update_queue', + 'pass_job_queue', + 'pass_user_data', + 'pass_chat_data', + 'run_async', + '__dict__', + ) + def __init__( self, callback: Callable[[UT, 'CallbackContext'], RT], @@ -106,6 +129,17 @@ class Handler(Generic[UT], ABC): self.pass_chat_data = pass_chat_data self.run_async = run_async + def __setattr__(self, key: str, value: object) -> None: + # See comment on BaseFilter to know why this was done. + if key.startswith('__'): + key = f"_{self.__class__.__name__}{key}" + if issubclass(self.__class__, Handler) and not self.__class__.__module__.startswith( + 'telegram.ext.' + ): + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + @abstractmethod def check_update(self, update: object) -> Optional[Union[bool, object]]: """ diff --git a/telegram/ext/inlinequeryhandler.py b/telegram/ext/inlinequeryhandler.py index 9e3f6f279..3216345c3 100644 --- a/telegram/ext/inlinequeryhandler.py +++ b/telegram/ext/inlinequeryhandler.py @@ -129,6 +129,8 @@ class InlineQueryHandler(Handler[Update]): """ + __slots__ = ('pattern', 'chat_types', 'pass_groups', 'pass_groupdict') + def __init__( self, callback: Callable[[Update, 'CallbackContext'], RT], diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 9ea4722d2..2ea2fec5c 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -31,6 +31,7 @@ from apscheduler.job import Job as APSJob from telegram.ext.callbackcontext import CallbackContext from telegram.utils.types import JSONDict +from telegram.utils.deprecate import set_new_attribute_deprecated if TYPE_CHECKING: from telegram import Bot @@ -49,6 +50,8 @@ class JobQueue: """ + __slots__ = ('_dispatcher', 'logger', 'scheduler', '__dict__') + def __init__(self) -> None: self._dispatcher: 'Dispatcher' = None # type: ignore[assignment] self.logger = logging.getLogger(self.__class__.__name__) @@ -64,6 +67,9 @@ class JobQueue: logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter) self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR) + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]: if self._dispatcher.use_context: return [CallbackContext.from_job(job, self._dispatcher)] @@ -542,6 +548,17 @@ class Job: job (:class:`apscheduler.job.Job`): Optional. The APS Job this job is a wrapper for. """ + __slots__ = ( + 'callback', + 'context', + 'name', + 'job_queue', + '_removed', + '_enabled', + 'job', + '__dict__', + ) + def __init__( self, callback: Callable[['CallbackContext'], None], @@ -561,6 +578,9 @@ class Job: self.job = cast(APSJob, job) # skipcq: PTC-W0052 + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + def run(self, dispatcher: 'Dispatcher') -> None: """Executes the callback function independently of the jobs schedule.""" try: diff --git a/telegram/ext/messagehandler.py b/telegram/ext/messagehandler.py index 270c40481..ed1078124 100644 --- a/telegram/ext/messagehandler.py +++ b/telegram/ext/messagehandler.py @@ -120,6 +120,8 @@ class MessageHandler(Handler[Update]): """ + __slots__ = ('filters',) + def __init__( self, filters: BaseFilter, diff --git a/telegram/ext/messagequeue.py b/telegram/ext/messagequeue.py index 449396b71..ece0bc389 100644 --- a/telegram/ext/messagequeue.py +++ b/telegram/ext/messagequeue.py @@ -40,6 +40,8 @@ curtime = time.perf_counter class DelayQueueError(RuntimeError): """Indicates processing errors.""" + __slots__ = () + class DelayQueue(threading.Thread): """ diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index 94fcb256a..3127d3baf 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -73,6 +73,16 @@ class PicklePersistence(BasePersistence): Default is :obj:`False`. """ + __slots__ = ( + 'filename', + 'single_file', + 'on_flush', + 'user_data', + 'chat_data', + 'bot_data', + 'conversations', + ) + def __init__( self, filename: str, diff --git a/telegram/ext/pollanswerhandler.py b/telegram/ext/pollanswerhandler.py index 891b64fee..73cd36bce 100644 --- a/telegram/ext/pollanswerhandler.py +++ b/telegram/ext/pollanswerhandler.py @@ -82,6 +82,8 @@ class PollAnswerHandler(Handler[Update]): """ + __slots__ = () + def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. diff --git a/telegram/ext/pollhandler.py b/telegram/ext/pollhandler.py index bc0d53600..c0719d5e5 100644 --- a/telegram/ext/pollhandler.py +++ b/telegram/ext/pollhandler.py @@ -82,6 +82,8 @@ class PollHandler(Handler[Update]): """ + __slots__ = () + def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. diff --git a/telegram/ext/precheckoutqueryhandler.py b/telegram/ext/precheckoutqueryhandler.py index 0a954e18a..1a93cccca 100644 --- a/telegram/ext/precheckoutqueryhandler.py +++ b/telegram/ext/precheckoutqueryhandler.py @@ -82,6 +82,8 @@ class PreCheckoutQueryHandler(Handler[Update]): """ + __slots__ = () + def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. diff --git a/telegram/ext/regexhandler.py b/telegram/ext/regexhandler.py index 4392c6e59..50b82a0e8 100644 --- a/telegram/ext/regexhandler.py +++ b/telegram/ext/regexhandler.py @@ -108,6 +108,8 @@ class RegexHandler(MessageHandler): """ + __slots__ = ('pass_groups', 'pass_groupdict') + def __init__( self, pattern: Union[str, Pattern], diff --git a/telegram/ext/shippingqueryhandler.py b/telegram/ext/shippingqueryhandler.py index 855163b44..53ea28494 100644 --- a/telegram/ext/shippingqueryhandler.py +++ b/telegram/ext/shippingqueryhandler.py @@ -81,6 +81,8 @@ class ShippingQueryHandler(Handler[Update]): """ + __slots__ = () + def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. diff --git a/telegram/ext/stringcommandhandler.py b/telegram/ext/stringcommandhandler.py index 0da3574fe..98aa6518b 100644 --- a/telegram/ext/stringcommandhandler.py +++ b/telegram/ext/stringcommandhandler.py @@ -85,6 +85,8 @@ class StringCommandHandler(Handler[str]): """ + __slots__ = ('command', 'pass_args') + def __init__( self, command: str, diff --git a/telegram/ext/stringregexhandler.py b/telegram/ext/stringregexhandler.py index 9af0910af..db3ee8440 100644 --- a/telegram/ext/stringregexhandler.py +++ b/telegram/ext/stringregexhandler.py @@ -91,6 +91,8 @@ class StringRegexHandler(Handler[str]): """ + __slots__ = ('pass_groups', 'pass_groupdict', 'pattern') + def __init__( self, pattern: Union[str, Pattern], diff --git a/telegram/ext/typehandler.py b/telegram/ext/typehandler.py index 8ff882cca..a4f7d7319 100644 --- a/telegram/ext/typehandler.py +++ b/telegram/ext/typehandler.py @@ -75,6 +75,8 @@ class TypeHandler(Handler[UT]): """ + __slots__ = ('type', 'strict') + def __init__( self, type: Type[UT], # pylint: disable=W0622 @@ -90,8 +92,8 @@ class TypeHandler(Handler[UT]): pass_job_queue=pass_job_queue, run_async=run_async, ) - self.type = type - self.strict = strict + self.type = type # pylint: disable=E0237 + self.strict = strict # pylint: disable=E0237 def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index aceb7da59..551bafe6d 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -30,7 +30,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Un from telegram import Bot, TelegramError from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized from telegram.ext import Dispatcher, JobQueue -from telegram.utils.deprecate import TelegramDeprecationWarning +from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated from telegram.utils.helpers import get_signal_name from telegram.utils.request import Request from telegram.ext.utils.webhookhandler import WebhookAppClass, WebhookServer @@ -105,7 +105,24 @@ class Updater: """ - _request = None + __slots__ = ( + 'persistence', + 'dispatcher', + 'user_sig_handler', + 'bot', + 'logger', + 'update_queue', + 'job_queue', + '__exception_event', + 'last_update_id', + 'running', + '_request', + 'is_idle', + 'httpd', + '__lock', + '__threads', + '__dict__', + ) def __init__( self, @@ -150,6 +167,7 @@ class Updater: raise ValueError('`dispatcher` and `use_context` are mutually exclusive') self.logger = logging.getLogger(__name__) + self._request = None if dispatcher is None: con_pool_size = workers + 4 @@ -219,6 +237,14 @@ class Updater: self.__lock = Lock() self.__threads: List[Thread] = [] + def __setattr__(self, key: str, value: object) -> None: + if key.startswith('__'): + key = f"_{self.__class__.__name__}{key}" + if issubclass(self.__class__, Updater) and self.__class__ is not Updater: + object.__setattr__(self, key, value) + return + set_new_attribute_deprecated(self, key, value) + def _init_thread(self, target: Callable, name: str, *args: object, **kwargs: object) -> None: thr = Thread( target=self._thread_wrapper, diff --git a/telegram/ext/utils/promise.py b/telegram/ext/utils/promise.py index 026d5f5ae..ff74bd56f 100644 --- a/telegram/ext/utils/promise.py +++ b/telegram/ext/utils/promise.py @@ -22,6 +22,7 @@ import logging from threading import Event from typing import Callable, List, Optional, Tuple, TypeVar, Union +from telegram.utils.deprecate import set_new_attribute_deprecated from telegram.utils.types import JSONDict RT = TypeVar('RT') @@ -54,6 +55,19 @@ class Promise: """ + __slots__ = ( + 'pooled_function', + 'args', + 'kwargs', + 'update', + 'error_handling', + 'done', + '_done_callback', + '_result', + '_exception', + '__dict__', + ) + # TODO: Remove error_handling parameter once we drop the @run_async decorator def __init__( self, @@ -73,6 +87,9 @@ class Promise: self._result: Optional[RT] = None self._exception: Optional[Exception] = None + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + def run(self) -> None: """Calls the :attr:`pooled_function` callable.""" try: diff --git a/telegram/ext/utils/webhookhandler.py b/telegram/ext/utils/webhookhandler.py index e96234d1a..5c4386da8 100644 --- a/telegram/ext/utils/webhookhandler.py +++ b/telegram/ext/utils/webhookhandler.py @@ -30,6 +30,7 @@ from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from telegram import Update +from telegram.utils.deprecate import set_new_attribute_deprecated from telegram.utils.types import JSONDict if TYPE_CHECKING: @@ -42,6 +43,18 @@ except ImportError: class WebhookServer: + __slots__ = ( + 'http_server', + 'listen', + 'port', + 'loop', + 'logger', + 'is_running', + 'server_lock', + 'shutdown_lock', + '__dict__', + ) + def __init__( self, listen: str, port: int, webhook_app: 'WebhookAppClass', ssl_ctx: SSLContext ): @@ -54,6 +67,9 @@ class WebhookServer: self.server_lock = Lock() self.shutdown_lock = Lock() + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + def serve_forever(self, ready: Event = None) -> None: with self.server_lock: IOLoop().make_current() diff --git a/telegram/files/animation.py b/telegram/files/animation.py index 5e3c9e651..199cf3328 100644 --- a/telegram/files/animation.py +++ b/telegram/files/animation.py @@ -65,6 +65,20 @@ class Animation(TelegramObject): """ + __slots__ = ( + 'bot', + 'width', + 'file_id', + 'file_size', + 'file_name', + 'thumb', + 'duration', + 'mime_type', + 'height', + 'file_unique_id', + '_id_attrs', + ) + def __init__( self, file_id: str, diff --git a/telegram/files/audio.py b/telegram/files/audio.py index 032b2a92d..d95711acd 100644 --- a/telegram/files/audio.py +++ b/telegram/files/audio.py @@ -69,6 +69,20 @@ class Audio(TelegramObject): """ + __slots__ = ( + 'file_id', + 'bot', + 'file_size', + 'file_name', + 'thumb', + 'title', + 'duration', + 'performer', + 'mime_type', + 'file_unique_id', + '_id_attrs', + ) + def __init__( self, file_id: str, diff --git a/telegram/files/chatphoto.py b/telegram/files/chatphoto.py index 2a40203c5..5302c7e98 100644 --- a/telegram/files/chatphoto.py +++ b/telegram/files/chatphoto.py @@ -65,6 +65,15 @@ class ChatPhoto(TelegramObject): """ + __slots__ = ( + 'big_file_unique_id', + 'bot', + 'small_file_id', + 'small_file_unique_id', + 'big_file_id', + '_id_attrs', + ) + def __init__( self, small_file_id: str, diff --git a/telegram/files/contact.py b/telegram/files/contact.py index 3467121c5..257fdf474 100644 --- a/telegram/files/contact.py +++ b/telegram/files/contact.py @@ -46,6 +46,8 @@ class Contact(TelegramObject): """ + __slots__ = ('vcard', 'user_id', 'first_name', 'last_name', 'phone_number', '_id_attrs') + def __init__( self, phone_number: str, diff --git a/telegram/files/document.py b/telegram/files/document.py index aca4047eb..dad9f9bf3 100644 --- a/telegram/files/document.py +++ b/telegram/files/document.py @@ -60,6 +60,17 @@ class Document(TelegramObject): """ + __slots__ = ( + 'bot', + 'file_id', + 'file_size', + 'file_name', + 'thumb', + 'mime_type', + 'file_unique_id', + '_id_attrs', + ) + _id_keys = ('file_id',) def __init__( diff --git a/telegram/files/file.py b/telegram/files/file.py index e8278c58f..c3391bd95 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -67,6 +67,16 @@ class File(TelegramObject): """ + __slots__ = ( + 'bot', + 'file_id', + 'file_size', + 'file_unique_id', + 'file_path', + '_credentials', + '_id_attrs', + ) + def __init__( self, file_id: str, diff --git a/telegram/files/inputfile.py b/telegram/files/inputfile.py index 43a8148ed..583f4a60d 100644 --- a/telegram/files/inputfile.py +++ b/telegram/files/inputfile.py @@ -26,6 +26,8 @@ import os from typing import IO, Optional, Tuple, Union from uuid import uuid4 +from telegram.utils.deprecate import set_new_attribute_deprecated + DEFAULT_MIME_TYPE = 'application/octet-stream' logger = logging.getLogger(__name__) @@ -50,6 +52,8 @@ class InputFile: """ + __slots__ = ('filename', 'attach', 'input_file_content', 'mimetype', '__dict__') + def __init__(self, obj: Union[IO, bytes], filename: str = None, attach: bool = None): self.filename = None if isinstance(obj, bytes): @@ -74,6 +78,9 @@ class InputFile: if not self.filename: self.filename = self.mimetype.replace('/', '.') + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + @property def field_tuple(self) -> Tuple[str, bytes, str]: # skipcq: PY-D0003 return self.filename, self.input_file_content, self.mimetype diff --git a/telegram/files/inputmedia.py b/telegram/files/inputmedia.py index a9dec806a..f59cf4d01 100644 --- a/telegram/files/inputmedia.py +++ b/telegram/files/inputmedia.py @@ -43,6 +43,7 @@ class InputMedia(TelegramObject): """ + __slots__ = () caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...], None] = None def to_dict(self) -> JSONDict: @@ -113,6 +114,18 @@ class InputMediaAnimation(InputMedia): """ + __slots__ = ( + 'caption_entities', + 'width', + 'media', + 'thumb', + 'caption', + 'duration', + 'parse_mode', + 'height', + 'type', + ) + def __init__( self, media: Union[FileInput, Animation], @@ -185,6 +198,8 @@ class InputMediaPhoto(InputMedia): """ + __slots__ = ('caption_entities', 'media', 'caption', 'parse_mode', 'type') + def __init__( self, media: Union[FileInput, PhotoSize], @@ -265,6 +280,19 @@ class InputMediaVideo(InputMedia): """ + __slots__ = ( + 'caption_entities', + 'width', + 'media', + 'thumb', + 'supports_streaming', + 'caption', + 'duration', + 'parse_mode', + 'height', + 'type', + ) + def __init__( self, media: Union[FileInput, Video], @@ -364,6 +392,18 @@ class InputMediaAudio(InputMedia): """ + __slots__ = ( + 'caption_entities', + 'media', + 'thumb', + 'caption', + 'title', + 'duration', + 'type', + 'parse_mode', + 'performer', + ) + def __init__( self, media: Union[FileInput, Audio], @@ -452,6 +492,16 @@ class InputMediaDocument(InputMedia): """ + __slots__ = ( + 'caption_entities', + 'media', + 'thumb', + 'caption', + 'parse_mode', + 'type', + 'disable_content_type_detection', + ) + def __init__( self, media: Union[FileInput, Document], diff --git a/telegram/files/location.py b/telegram/files/location.py index 1a2f371f7..8f5c1c63d 100644 --- a/telegram/files/location.py +++ b/telegram/files/location.py @@ -56,6 +56,16 @@ class Location(TelegramObject): """ + __slots__ = ( + 'longitude', + 'horizontal_accuracy', + 'proximity_alert_radius', + 'live_period', + 'latitude', + 'heading', + '_id_attrs', + ) + def __init__( self, longitude: float, diff --git a/telegram/files/photosize.py b/telegram/files/photosize.py index 1456a8060..831a7c011 100644 --- a/telegram/files/photosize.py +++ b/telegram/files/photosize.py @@ -58,6 +58,8 @@ class PhotoSize(TelegramObject): """ + __slots__ = ('bot', 'width', 'file_id', 'file_size', 'height', 'file_unique_id', '_id_attrs') + def __init__( self, file_id: str, diff --git a/telegram/files/sticker.py b/telegram/files/sticker.py index 429a12f75..681c7087b 100644 --- a/telegram/files/sticker.py +++ b/telegram/files/sticker.py @@ -73,6 +73,21 @@ class Sticker(TelegramObject): """ + __slots__ = ( + 'bot', + 'width', + 'file_id', + 'is_animated', + 'file_size', + 'thumb', + 'set_name', + 'mask_position', + 'height', + 'file_unique_id', + 'emoji', + '_id_attrs', + ) + def __init__( self, file_id: str, @@ -160,6 +175,16 @@ class StickerSet(TelegramObject): """ + __slots__ = ( + 'is_animated', + 'contains_masks', + 'thumb', + 'title', + 'stickers', + 'name', + '_id_attrs', + ) + def __init__( self, name: str, @@ -233,6 +258,8 @@ class MaskPosition(TelegramObject): """ + __slots__ = ('point', 'scale', 'x_shift', 'y_shift', '_id_attrs') + FOREHEAD: ClassVar[str] = constants.STICKER_FOREHEAD """:const:`telegram.constants.STICKER_FOREHEAD`""" EYES: ClassVar[str] = constants.STICKER_EYES diff --git a/telegram/files/venue.py b/telegram/files/venue.py index 28e9fa107..3ba2c53a3 100644 --- a/telegram/files/venue.py +++ b/telegram/files/venue.py @@ -60,6 +60,17 @@ class Venue(TelegramObject): """ + __slots__ = ( + 'google_place_type', + 'location', + 'title', + 'address', + 'foursquare_type', + 'foursquare_id', + 'google_place_id', + '_id_attrs', + ) + def __init__( self, location: Location, diff --git a/telegram/files/video.py b/telegram/files/video.py index b21738cf4..76bb07cda 100644 --- a/telegram/files/video.py +++ b/telegram/files/video.py @@ -66,6 +66,20 @@ class Video(TelegramObject): """ + __slots__ = ( + 'bot', + 'width', + 'file_id', + 'file_size', + 'file_name', + 'thumb', + 'duration', + 'mime_type', + 'height', + 'file_unique_id', + '_id_attrs', + ) + def __init__( self, file_id: str, diff --git a/telegram/files/videonote.py b/telegram/files/videonote.py index 6052b9a1d..8c704069e 100644 --- a/telegram/files/videonote.py +++ b/telegram/files/videonote.py @@ -61,6 +61,17 @@ class VideoNote(TelegramObject): """ + __slots__ = ( + 'bot', + 'length', + 'file_id', + 'file_size', + 'thumb', + 'duration', + 'file_unique_id', + '_id_attrs', + ) + def __init__( self, file_id: str, diff --git a/telegram/files/voice.py b/telegram/files/voice.py index 185ab2970..f65c5c590 100644 --- a/telegram/files/voice.py +++ b/telegram/files/voice.py @@ -58,6 +58,16 @@ class Voice(TelegramObject): """ + __slots__ = ( + 'bot', + 'file_id', + 'file_size', + 'duration', + 'mime_type', + 'file_unique_id', + '_id_attrs', + ) + def __init__( self, file_id: str, diff --git a/telegram/forcereply.py b/telegram/forcereply.py index c3dd87db7..aaf7c733a 100644 --- a/telegram/forcereply.py +++ b/telegram/forcereply.py @@ -50,6 +50,8 @@ class ForceReply(ReplyMarkup): """ + __slots__ = ('selective', 'force_reply', '_id_attrs') + def __init__(self, force_reply: bool = True, selective: bool = False, **_kwargs: Any): # Required self.force_reply = bool(force_reply) diff --git a/telegram/games/callbackgame.py b/telegram/games/callbackgame.py index c82f54b7e..8a116dab5 100644 --- a/telegram/games/callbackgame.py +++ b/telegram/games/callbackgame.py @@ -23,3 +23,5 @@ from telegram import TelegramObject class CallbackGame(TelegramObject): """A placeholder, currently holds no information. Use BotFather to set up your game.""" + + __slots__ = () diff --git a/telegram/games/game.py b/telegram/games/game.py index dbbb6e5c8..185287347 100644 --- a/telegram/games/game.py +++ b/telegram/games/game.py @@ -67,6 +67,16 @@ class Game(TelegramObject): """ + __slots__ = ( + 'title', + 'photo', + 'description', + 'text_entities', + 'text', + 'animation', + '_id_attrs', + ) + def __init__( self, title: str, diff --git a/telegram/games/gamehighscore.py b/telegram/games/gamehighscore.py index 8eef2d6bd..bfa7cbfbf 100644 --- a/telegram/games/gamehighscore.py +++ b/telegram/games/gamehighscore.py @@ -45,6 +45,8 @@ class GameHighScore(TelegramObject): """ + __slots__ = ('position', 'user', 'score', '_id_attrs') + def __init__(self, position: int, user: User, score: int): self.position = position self.user = user diff --git a/telegram/inline/inlinekeyboardbutton.py b/telegram/inline/inlinekeyboardbutton.py index 2b5ec88dc..ed3a51bae 100644 --- a/telegram/inline/inlinekeyboardbutton.py +++ b/telegram/inline/inlinekeyboardbutton.py @@ -83,6 +83,18 @@ class InlineKeyboardButton(TelegramObject): """ + __slots__ = ( + 'callback_game', + 'url', + 'switch_inline_query_current_chat', + 'callback_data', + 'pay', + 'switch_inline_query', + 'text', + '_id_attrs', + 'login_url', + ) + def __init__( self, text: str, diff --git a/telegram/inline/inlinekeyboardmarkup.py b/telegram/inline/inlinekeyboardmarkup.py index 70c57ec9a..a917d96f3 100644 --- a/telegram/inline/inlinekeyboardmarkup.py +++ b/telegram/inline/inlinekeyboardmarkup.py @@ -45,6 +45,8 @@ class InlineKeyboardMarkup(ReplyMarkup): """ + __slots__ = ('inline_keyboard', '_id_attrs') + def __init__(self, inline_keyboard: List[List[InlineKeyboardButton]], **_kwargs: Any): # Required self.inline_keyboard = inline_keyboard diff --git a/telegram/inline/inlinequery.py b/telegram/inline/inlinequery.py index e4dd03c21..412188db4 100644 --- a/telegram/inline/inlinequery.py +++ b/telegram/inline/inlinequery.py @@ -71,6 +71,8 @@ class InlineQuery(TelegramObject): """ + __slots__ = ('bot', 'location', 'chat_type', 'id', 'offset', 'from_user', 'query', '_id_attrs') + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresult.py b/telegram/inline/inlinequeryresult.py index d1e06866a..756e2fb9c 100644 --- a/telegram/inline/inlinequeryresult.py +++ b/telegram/inline/inlinequeryresult.py @@ -46,6 +46,8 @@ class InlineQueryResult(TelegramObject): """ + __slots__ = ('type', 'id', '_id_attrs') + def __init__(self, type: str, id: str, **_kwargs: Any): # Required self.type = str(type) diff --git a/telegram/inline/inlinequeryresultarticle.py b/telegram/inline/inlinequeryresultarticle.py index 6d17544bb..3827ae305 100644 --- a/telegram/inline/inlinequeryresultarticle.py +++ b/telegram/inline/inlinequeryresultarticle.py @@ -63,6 +63,18 @@ class InlineQueryResultArticle(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'thumb_width', + 'thumb_height', + 'hide_url', + 'url', + 'title', + 'description', + 'input_message_content', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultaudio.py b/telegram/inline/inlinequeryresultaudio.py index 91d329e30..93eaa1649 100644 --- a/telegram/inline/inlinequeryresultaudio.py +++ b/telegram/inline/inlinequeryresultaudio.py @@ -74,6 +74,18 @@ class InlineQueryResultAudio(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'parse_mode', + 'audio_url', + 'performer', + 'input_message_content', + 'audio_duration', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcachedaudio.py b/telegram/inline/inlinequeryresultcachedaudio.py index ed8b4b233..41222bbb6 100644 --- a/telegram/inline/inlinequeryresultcachedaudio.py +++ b/telegram/inline/inlinequeryresultcachedaudio.py @@ -68,6 +68,15 @@ class InlineQueryResultCachedAudio(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'parse_mode', + 'audio_file_id', + 'input_message_content', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcacheddocument.py b/telegram/inline/inlinequeryresultcacheddocument.py index 07a0d5127..784ccaffb 100644 --- a/telegram/inline/inlinequeryresultcacheddocument.py +++ b/telegram/inline/inlinequeryresultcacheddocument.py @@ -75,6 +75,17 @@ class InlineQueryResultCachedDocument(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'document_file_id', + 'caption', + 'title', + 'description', + 'parse_mode', + 'input_message_content', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcachedgif.py b/telegram/inline/inlinequeryresultcachedgif.py index d9ddb4a7a..ca2fc4210 100644 --- a/telegram/inline/inlinequeryresultcachedgif.py +++ b/telegram/inline/inlinequeryresultcachedgif.py @@ -73,6 +73,16 @@ class InlineQueryResultCachedGif(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'input_message_content', + 'parse_mode', + 'gif_file_id', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcachedmpeg4gif.py b/telegram/inline/inlinequeryresultcachedmpeg4gif.py index 3104eb772..4f0f85cf5 100644 --- a/telegram/inline/inlinequeryresultcachedmpeg4gif.py +++ b/telegram/inline/inlinequeryresultcachedmpeg4gif.py @@ -73,6 +73,16 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'mpeg4_file_id', + 'caption', + 'title', + 'parse_mode', + 'input_message_content', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcachedphoto.py b/telegram/inline/inlinequeryresultcachedphoto.py index c3857937b..4a929dd2b 100644 --- a/telegram/inline/inlinequeryresultcachedphoto.py +++ b/telegram/inline/inlinequeryresultcachedphoto.py @@ -76,6 +76,17 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'description', + 'parse_mode', + 'photo_file_id', + 'input_message_content', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcachedsticker.py b/telegram/inline/inlinequeryresultcachedsticker.py index 0bb317e05..f369bdd4a 100644 --- a/telegram/inline/inlinequeryresultcachedsticker.py +++ b/telegram/inline/inlinequeryresultcachedsticker.py @@ -52,6 +52,8 @@ class InlineQueryResultCachedSticker(InlineQueryResult): """ + __slots__ = ('reply_markup', 'input_message_content', 'sticker_file_id') + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcachedvideo.py b/telegram/inline/inlinequeryresultcachedvideo.py index 6e46dc088..ee91515f1 100644 --- a/telegram/inline/inlinequeryresultcachedvideo.py +++ b/telegram/inline/inlinequeryresultcachedvideo.py @@ -75,6 +75,17 @@ class InlineQueryResultCachedVideo(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'description', + 'parse_mode', + 'input_message_content', + 'video_file_id', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcachedvoice.py b/telegram/inline/inlinequeryresultcachedvoice.py index b99817d10..ff2ef2270 100644 --- a/telegram/inline/inlinequeryresultcachedvoice.py +++ b/telegram/inline/inlinequeryresultcachedvoice.py @@ -70,6 +70,16 @@ class InlineQueryResultCachedVoice(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'parse_mode', + 'voice_file_id', + 'input_message_content', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultcontact.py b/telegram/inline/inlinequeryresultcontact.py index 9a9e9c873..42dd75d4b 100644 --- a/telegram/inline/inlinequeryresultcontact.py +++ b/telegram/inline/inlinequeryresultcontact.py @@ -66,6 +66,18 @@ class InlineQueryResultContact(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'thumb_width', + 'thumb_height', + 'vcard', + 'first_name', + 'last_name', + 'phone_number', + 'input_message_content', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultdocument.py b/telegram/inline/inlinequeryresultdocument.py index 26328ff47..4e3c0b0b2 100644 --- a/telegram/inline/inlinequeryresultdocument.py +++ b/telegram/inline/inlinequeryresultdocument.py @@ -85,6 +85,21 @@ class InlineQueryResultDocument(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'document_url', + 'thumb_width', + 'thumb_height', + 'caption', + 'title', + 'description', + 'parse_mode', + 'mime_type', + 'thumb_url', + 'input_message_content', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultgame.py b/telegram/inline/inlinequeryresultgame.py index 7ae3b43ec..f8535b44b 100644 --- a/telegram/inline/inlinequeryresultgame.py +++ b/telegram/inline/inlinequeryresultgame.py @@ -45,6 +45,8 @@ class InlineQueryResultGame(InlineQueryResult): """ + __slots__ = ('reply_markup', 'game_short_name') + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultgif.py b/telegram/inline/inlinequeryresultgif.py index c1888b5a6..619af4508 100644 --- a/telegram/inline/inlinequeryresultgif.py +++ b/telegram/inline/inlinequeryresultgif.py @@ -86,6 +86,21 @@ class InlineQueryResultGif(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'gif_height', + 'thumb_mime_type', + 'caption_entities', + 'gif_width', + 'title', + 'caption', + 'parse_mode', + 'gif_duration', + 'input_message_content', + 'gif_url', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultlocation.py b/telegram/inline/inlinequeryresultlocation.py index 9b3f4c433..2591b6361 100644 --- a/telegram/inline/inlinequeryresultlocation.py +++ b/telegram/inline/inlinequeryresultlocation.py @@ -79,6 +79,21 @@ class InlineQueryResultLocation(InlineQueryResult): """ + __slots__ = ( + 'longitude', + 'reply_markup', + 'thumb_width', + 'thumb_height', + 'heading', + 'title', + 'live_period', + 'proximity_alert_radius', + 'input_message_content', + 'latitude', + 'horizontal_accuracy', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultmpeg4gif.py b/telegram/inline/inlinequeryresultmpeg4gif.py index 9ddd44f4a..3eb1c21f3 100644 --- a/telegram/inline/inlinequeryresultmpeg4gif.py +++ b/telegram/inline/inlinequeryresultmpeg4gif.py @@ -85,6 +85,21 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'thumb_mime_type', + 'caption_entities', + 'mpeg4_duration', + 'mpeg4_width', + 'title', + 'caption', + 'parse_mode', + 'input_message_content', + 'mpeg4_url', + 'mpeg4_height', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultphoto.py b/telegram/inline/inlinequeryresultphoto.py index 4f09254d3..98f718562 100644 --- a/telegram/inline/inlinequeryresultphoto.py +++ b/telegram/inline/inlinequeryresultphoto.py @@ -82,6 +82,20 @@ class InlineQueryResultPhoto(InlineQueryResult): """ + __slots__ = ( + 'photo_url', + 'reply_markup', + 'caption_entities', + 'photo_width', + 'caption', + 'title', + 'description', + 'parse_mode', + 'input_message_content', + 'photo_height', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultvenue.py b/telegram/inline/inlinequeryresultvenue.py index eba6b163c..9930f7ab7 100644 --- a/telegram/inline/inlinequeryresultvenue.py +++ b/telegram/inline/inlinequeryresultvenue.py @@ -79,6 +79,22 @@ class InlineQueryResultVenue(InlineQueryResult): """ + __slots__ = ( + 'longitude', + 'reply_markup', + 'google_place_type', + 'thumb_width', + 'thumb_height', + 'title', + 'address', + 'foursquare_id', + 'foursquare_type', + 'google_place_id', + 'input_message_content', + 'latitude', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultvideo.py b/telegram/inline/inlinequeryresultvideo.py index 0245d6fba..b84a3f2b9 100644 --- a/telegram/inline/inlinequeryresultvideo.py +++ b/telegram/inline/inlinequeryresultvideo.py @@ -92,6 +92,22 @@ class InlineQueryResultVideo(InlineQueryResult): """ + __slots__ = ( + 'video_url', + 'reply_markup', + 'caption_entities', + 'caption', + 'title', + 'description', + 'video_duration', + 'parse_mode', + 'mime_type', + 'input_message_content', + 'video_height', + 'video_width', + 'thumb_url', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inlinequeryresultvoice.py b/telegram/inline/inlinequeryresultvoice.py index a38aff13c..531f04b23 100644 --- a/telegram/inline/inlinequeryresultvoice.py +++ b/telegram/inline/inlinequeryresultvoice.py @@ -73,6 +73,17 @@ class InlineQueryResultVoice(InlineQueryResult): """ + __slots__ = ( + 'reply_markup', + 'caption_entities', + 'voice_duration', + 'caption', + 'title', + 'voice_url', + 'parse_mode', + 'input_message_content', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/inline/inputcontactmessagecontent.py b/telegram/inline/inputcontactmessagecontent.py index 0edb6d0d5..22e9460c7 100644 --- a/telegram/inline/inputcontactmessagecontent.py +++ b/telegram/inline/inputcontactmessagecontent.py @@ -46,6 +46,8 @@ class InputContactMessageContent(InputMessageContent): """ + __slots__ = ('vcard', 'first_name', 'last_name', 'phone_number', '_id_attrs') + def __init__( self, phone_number: str, diff --git a/telegram/inline/inputinvoicemessagecontent.py b/telegram/inline/inputinvoicemessagecontent.py index 849451fbd..2cbbcb8f4 100644 --- a/telegram/inline/inputinvoicemessagecontent.py +++ b/telegram/inline/inputinvoicemessagecontent.py @@ -123,6 +123,30 @@ class InputInvoiceMessageContent(InputMessageContent): """ + __slots__ = ( + 'title', + 'description', + 'payload', + 'provider_token', + 'currency', + 'prices', + 'max_tip_amount', + 'suggested_tip_amounts', + 'provider_data', + 'photo_url', + 'photo_size', + 'photo_width', + 'photo_height', + 'need_name', + 'need_phone_number', + 'need_email', + 'need_shipping_address', + 'send_phone_number_to_provider', + 'send_email_to_provider', + 'is_flexible', + '_id_attrs', + ) + def __init__( self, title: str, diff --git a/telegram/inline/inputlocationmessagecontent.py b/telegram/inline/inputlocationmessagecontent.py index 397acd0a0..fe8662882 100644 --- a/telegram/inline/inputlocationmessagecontent.py +++ b/telegram/inline/inputlocationmessagecontent.py @@ -59,6 +59,8 @@ class InputLocationMessageContent(InputMessageContent): """ + __slots__ = ('longitude', 'horizontal_accuracy', 'proximity_alert_radius', 'live_period', + 'latitude', 'heading', '_id_attrs') # fmt: on def __init__( diff --git a/telegram/inline/inputmessagecontent.py b/telegram/inline/inputmessagecontent.py index 5a803bb6f..44fd6811a 100644 --- a/telegram/inline/inputmessagecontent.py +++ b/telegram/inline/inputmessagecontent.py @@ -30,3 +30,5 @@ class InputMessageContent(TelegramObject): :class:`telegram.InputVenueMessageContent` for more details. """ + + __slots__ = () diff --git a/telegram/inline/inputtextmessagecontent.py b/telegram/inline/inputtextmessagecontent.py index b71f5b51f..3d60f456c 100644 --- a/telegram/inline/inputtextmessagecontent.py +++ b/telegram/inline/inputtextmessagecontent.py @@ -59,6 +59,8 @@ class InputTextMessageContent(InputMessageContent): """ + __slots__ = ('disable_web_page_preview', 'parse_mode', 'entities', 'message_text', '_id_attrs') + def __init__( self, message_text: str, diff --git a/telegram/inline/inputvenuemessagecontent.py b/telegram/inline/inputvenuemessagecontent.py index 6e5dbd0d0..55652d2a9 100644 --- a/telegram/inline/inputvenuemessagecontent.py +++ b/telegram/inline/inputvenuemessagecontent.py @@ -60,6 +60,18 @@ class InputVenueMessageContent(InputMessageContent): """ + __slots__ = ( + 'longitude', + 'google_place_type', + 'title', + 'address', + 'foursquare_id', + 'foursquare_type', + 'google_place_id', + 'latitude', + '_id_attrs', + ) + def __init__( self, latitude: float, diff --git a/telegram/keyboardbutton.py b/telegram/keyboardbutton.py index 0632bb257..590801b2c 100644 --- a/telegram/keyboardbutton.py +++ b/telegram/keyboardbutton.py @@ -58,6 +58,8 @@ class KeyboardButton(TelegramObject): """ + __slots__ = ('request_location', 'request_contact', 'request_poll', 'text', '_id_attrs') + def __init__( self, text: str, diff --git a/telegram/keyboardbuttonpolltype.py b/telegram/keyboardbuttonpolltype.py index 49de3040f..89be62a02 100644 --- a/telegram/keyboardbuttonpolltype.py +++ b/telegram/keyboardbuttonpolltype.py @@ -37,6 +37,8 @@ class KeyboardButtonPollType(TelegramObject): create a poll of any type. """ + __slots__ = ('type', '_id_attrs') + def __init__(self, type: str = None, **_kwargs: Any): # pylint: disable=W0622 self.type = type diff --git a/telegram/loginurl.py b/telegram/loginurl.py index 2c385bf2b..81792735b 100644 --- a/telegram/loginurl.py +++ b/telegram/loginurl.py @@ -69,6 +69,8 @@ class LoginUrl(TelegramObject): """ + __slots__ = ('bot_username', 'request_write_access', 'url', 'forward_text', '_id_attrs') + def __init__( self, url: str, diff --git a/telegram/message.py b/telegram/message.py index 7bb34486a..63e18bf80 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -77,8 +77,6 @@ if TYPE_CHECKING: LabeledPrice, ) -_UNDEFINED = object() - class Message(TelegramObject): # fmt: off @@ -333,8 +331,67 @@ class Message(TelegramObject): """ # fmt: on - - _effective_attachment = _UNDEFINED + __slots__ = ( + 'reply_markup', + 'audio', + 'contact', + 'migrate_to_chat_id', + 'forward_signature', + 'chat', + 'successful_payment', + 'game', + 'text', + 'forward_sender_name', + 'document', + 'new_chat_title', + 'forward_date', + 'group_chat_created', + 'media_group_id', + 'caption', + 'video', + 'bot', + 'entities', + 'via_bot', + 'new_chat_members', + 'connected_website', + 'animation', + 'migrate_from_chat_id', + 'forward_from', + 'sticker', + 'location', + 'venue', + 'edit_date', + 'reply_to_message', + 'passport_data', + 'pinned_message', + 'forward_from_chat', + 'new_chat_photo', + 'message_id', + 'delete_chat_photo', + 'from_user', + 'author_signature', + 'proximity_alert_triggered', + 'sender_chat', + 'dice', + 'forward_from_message_id', + 'caption_entities', + 'voice', + 'date', + 'supergroup_chat_created', + 'poll', + 'left_chat_member', + 'photo', + 'channel_chat_created', + 'invoice', + 'video_note', + '_effective_attachment', + 'message_auto_delete_timer_changed', + 'voice_chat_ended', + 'voice_chat_participants_invited', + 'voice_chat_started', + 'voice_chat_scheduled', + '_id_attrs', + ) ATTACHMENT_TYPES: ClassVar[List[str]] = [ 'audio', @@ -497,6 +554,8 @@ class Message(TelegramObject): self.reply_markup = reply_markup self.bot = bot + self._effective_attachment = DEFAULT_NONE + self._id_attrs = (self.message_id, self.chat) @property @@ -613,7 +672,7 @@ class Message(TelegramObject): :obj:`None` if no attachment was sent. """ - if self._effective_attachment is not _UNDEFINED: + if self._effective_attachment is not DEFAULT_NONE: return self._effective_attachment # type: ignore for i in Message.ATTACHMENT_TYPES: @@ -626,10 +685,7 @@ class Message(TelegramObject): return self._effective_attachment # type: ignore def __getitem__(self, item: str) -> Any: # pylint: disable=R1710 - if item in self.__dict__.keys(): - return self.__dict__[item] - if item == 'chat_id': - return self.chat.id + return self.chat.id if item == 'chat_id' else super().__getitem__(item) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" diff --git a/telegram/messageautodeletetimerchanged.py b/telegram/messageautodeletetimerchanged.py index 6a198ce30..3fb1ce919 100644 --- a/telegram/messageautodeletetimerchanged.py +++ b/telegram/messageautodeletetimerchanged.py @@ -44,6 +44,8 @@ class MessageAutoDeleteTimerChanged(TelegramObject): """ + __slots__ = ('message_auto_delete_time', '_id_attrs') + def __init__( self, message_auto_delete_time: int, diff --git a/telegram/messageentity.py b/telegram/messageentity.py index 59a9f2118..0a0350eeb 100644 --- a/telegram/messageentity.py +++ b/telegram/messageentity.py @@ -59,6 +59,8 @@ class MessageEntity(TelegramObject): """ + __slots__ = ('length', 'url', 'user', 'type', 'language', 'offset', '_id_attrs') + def __init__( self, type: str, # pylint: disable=W0622 diff --git a/telegram/messageid.py b/telegram/messageid.py index 3b78ab93d..56eca3a19 100644 --- a/telegram/messageid.py +++ b/telegram/messageid.py @@ -32,6 +32,8 @@ class MessageId(TelegramObject): message_id (:obj:`int`): Unique message identifier """ + __slots__ = ('message_id', '_id_attrs') + def __init__(self, message_id: int, **_kwargs: Any): self.message_id = int(message_id) diff --git a/telegram/parsemode.py b/telegram/parsemode.py index 27ba5b36e..86bc07b36 100644 --- a/telegram/parsemode.py +++ b/telegram/parsemode.py @@ -21,11 +21,14 @@ from typing import ClassVar from telegram import constants +from telegram.utils.deprecate import set_new_attribute_deprecated class ParseMode: """This object represents a Telegram Message Parse Modes.""" + __slots__ = ('__dict__',) + MARKDOWN: ClassVar[str] = constants.PARSEMODE_MARKDOWN """:const:`telegram.constants.PARSEMODE_MARKDOWN`\n @@ -37,3 +40,6 @@ class ParseMode: """:const:`telegram.constants.PARSEMODE_MARKDOWN_V2`""" HTML: ClassVar[str] = constants.PARSEMODE_HTML """:const:`telegram.constants.PARSEMODE_HTML`""" + + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) diff --git a/telegram/passport/credentials.py b/telegram/passport/credentials.py index 861edb53f..156c79de8 100644 --- a/telegram/passport/credentials.py +++ b/telegram/passport/credentials.py @@ -51,6 +51,8 @@ if TYPE_CHECKING: class TelegramDecryptionError(TelegramError): """Something went wrong with decryption.""" + __slots__ = ('_msg',) + def __init__(self, message: Union[str, Exception]): super().__init__(f"TelegramDecryptionError: {message}") self._msg = str(message) @@ -143,6 +145,16 @@ class EncryptedCredentials(TelegramObject): """ + __slots__ = ( + 'hash', + 'secret', + 'bot', + 'data', + '_id_attrs', + '_decrypted_secret', + '_decrypted_data', + ) + def __init__(self, data: str, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any): # Required self.data = data @@ -212,6 +224,8 @@ class Credentials(TelegramObject): nonce (:obj:`str`): Bot-specified nonce """ + __slots__ = ('bot', 'nonce', 'secure_data') + def __init__(self, secure_data: 'SecureData', nonce: str, bot: 'Bot' = None, **_kwargs: Any): # Required self.secure_data = secure_data @@ -260,6 +274,21 @@ class SecureData(TelegramObject): temporary registration. """ + __slots__ = ( + 'bot', + 'utility_bill', + 'personal_details', + 'temporary_registration', + 'address', + 'driver_license', + 'rental_agreement', + 'internal_passport', + 'identity_card', + 'bank_statement', + 'passport', + 'passport_registration', + ) + def __init__( self, personal_details: 'SecureValue' = None, @@ -345,6 +374,8 @@ class SecureValue(TelegramObject): """ + __slots__ = ('data', 'front_side', 'reverse_side', 'selfie', 'files', 'translation', 'bot') + def __init__( self, data: 'DataCredentials' = None, @@ -395,6 +426,8 @@ class SecureValue(TelegramObject): class _CredentialsBase(TelegramObject): """Base class for DataCredentials and FileCredentials.""" + __slots__ = ('hash', 'secret', 'file_hash', 'data_hash', 'bot') + def __init__(self, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any): self.hash = hash self.secret = secret @@ -420,6 +453,8 @@ class DataCredentials(_CredentialsBase): secret (:obj:`str`): Secret of encrypted data """ + __slots__ = () + def __init__(self, data_hash: str, secret: str, **_kwargs: Any): super().__init__(data_hash, secret, **_kwargs) @@ -447,6 +482,8 @@ class FileCredentials(_CredentialsBase): secret (:obj:`str`): Secret of encrypted file """ + __slots__ = () + def __init__(self, file_hash: str, secret: str, **_kwargs: Any): super().__init__(file_hash, secret, **_kwargs) diff --git a/telegram/passport/data.py b/telegram/passport/data.py index 21bf3c513..b17f5d87f 100644 --- a/telegram/passport/data.py +++ b/telegram/passport/data.py @@ -46,6 +46,20 @@ class PersonalDetails(TelegramObject): residence. """ + __slots__ = ( + 'middle_name', + 'first_name_native', + 'last_name_native', + 'residence_country_code', + 'first_name', + 'last_name', + 'country_code', + 'gender', + 'bot', + 'middle_name_native', + 'birth_date', + ) + def __init__( self, first_name: str, @@ -89,6 +103,16 @@ class ResidentialAddress(TelegramObject): post_code (:obj:`str`): Address post code. """ + __slots__ = ( + 'post_code', + 'city', + 'country_code', + 'street_line2', + 'street_line1', + 'bot', + 'state', + ) + def __init__( self, street_line1: str, @@ -120,6 +144,8 @@ class IdDocumentData(TelegramObject): expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format. """ + __slots__ = ('document_no', 'bot', 'expiry_date') + def __init__(self, document_no: str, expiry_date: str, bot: 'Bot' = None, **_kwargs: Any): self.document_no = document_no self.expiry_date = expiry_date diff --git a/telegram/passport/encryptedpassportelement.py b/telegram/passport/encryptedpassportelement.py index e25b14494..74e3aaf67 100644 --- a/telegram/passport/encryptedpassportelement.py +++ b/telegram/passport/encryptedpassportelement.py @@ -118,6 +118,21 @@ class EncryptedPassportElement(TelegramObject): """ + __slots__ = ( + 'selfie', + 'files', + 'type', + 'translation', + 'email', + 'hash', + 'phone_number', + 'bot', + 'reverse_side', + 'front_side', + 'data', + '_id_attrs', + ) + def __init__( self, type: str, # pylint: disable=W0622 diff --git a/telegram/passport/passportdata.py b/telegram/passport/passportdata.py index 81479d452..a8d1ede02 100644 --- a/telegram/passport/passportdata.py +++ b/telegram/passport/passportdata.py @@ -51,6 +51,8 @@ class PassportData(TelegramObject): """ + __slots__ = ('bot', 'credentials', 'data', '_decrypted_data', '_id_attrs') + def __init__( self, data: List[EncryptedPassportElement], diff --git a/telegram/passport/passportelementerrors.py b/telegram/passport/passportelementerrors.py index 610f6cbd5..4d61f962b 100644 --- a/telegram/passport/passportelementerrors.py +++ b/telegram/passport/passportelementerrors.py @@ -45,6 +45,9 @@ class PassportElementError(TelegramObject): """ + # All subclasses of this class won't have _id_attrs in slots since it's added here. + __slots__ = ('message', 'source', 'type', '_id_attrs') + def __init__(self, source: str, type: str, message: str, **_kwargs: Any): # Required self.source = str(source) @@ -82,6 +85,8 @@ class PassportElementErrorDataField(PassportElementError): """ + __slots__ = ('data_hash', 'field_name') + def __init__(self, type: str, field_name: str, data_hash: str, message: str, **_kwargs: Any): # Required super().__init__('data', type, message) @@ -117,6 +122,8 @@ class PassportElementErrorFile(PassportElementError): """ + __slots__ = ('file_hash',) + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required super().__init__('file', type, message) @@ -151,6 +158,8 @@ class PassportElementErrorFiles(PassportElementError): """ + __slots__ = ('file_hashes',) + def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any): # Required super().__init__('files', type, message) @@ -185,6 +194,8 @@ class PassportElementErrorFrontSide(PassportElementError): """ + __slots__ = ('file_hash',) + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required super().__init__('front_side', type, message) @@ -219,6 +230,8 @@ class PassportElementErrorReverseSide(PassportElementError): """ + __slots__ = ('file_hash',) + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required super().__init__('reverse_side', type, message) @@ -251,6 +264,8 @@ class PassportElementErrorSelfie(PassportElementError): """ + __slots__ = ('file_hash',) + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required super().__init__('selfie', type, message) @@ -287,6 +302,8 @@ class PassportElementErrorTranslationFile(PassportElementError): """ + __slots__ = ('file_hash',) + def __init__(self, type: str, file_hash: str, message: str, **_kwargs: Any): # Required super().__init__('translation_file', type, message) @@ -323,6 +340,8 @@ class PassportElementErrorTranslationFiles(PassportElementError): """ + __slots__ = ('file_hashes',) + def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any): # Required super().__init__('translation_files', type, message) @@ -353,6 +372,8 @@ class PassportElementErrorUnspecified(PassportElementError): """ + __slots__ = ('element_hash',) + def __init__(self, type: str, element_hash: str, message: str, **_kwargs: Any): # Required super().__init__('unspecified', type, message) diff --git a/telegram/passport/passportfile.py b/telegram/passport/passportfile.py index dce3f176f..b5f212200 100644 --- a/telegram/passport/passportfile.py +++ b/telegram/passport/passportfile.py @@ -58,6 +58,16 @@ class PassportFile(TelegramObject): """ + __slots__ = ( + 'file_date', + 'bot', + 'file_id', + 'file_size', + '_credentials', + 'file_unique_id', + '_id_attrs', + ) + def __init__( self, file_id: str, diff --git a/telegram/payment/invoice.py b/telegram/payment/invoice.py index 2dc478887..dea274035 100644 --- a/telegram/payment/invoice.py +++ b/telegram/payment/invoice.py @@ -53,6 +53,15 @@ class Invoice(TelegramObject): """ + __slots__ = ( + 'currency', + 'start_parameter', + 'title', + 'description', + 'total_amount', + '_id_attrs', + ) + def __init__( self, title: str, diff --git a/telegram/payment/labeledprice.py b/telegram/payment/labeledprice.py index ce69863ee..221c62dbc 100644 --- a/telegram/payment/labeledprice.py +++ b/telegram/payment/labeledprice.py @@ -45,6 +45,8 @@ class LabeledPrice(TelegramObject): """ + __slots__ = ('label', '_id_attrs', 'amount') + def __init__(self, label: str, amount: int, **_kwargs: Any): self.label = label self.amount = amount diff --git a/telegram/payment/orderinfo.py b/telegram/payment/orderinfo.py index 11d044833..7ebe35851 100644 --- a/telegram/payment/orderinfo.py +++ b/telegram/payment/orderinfo.py @@ -49,6 +49,8 @@ class OrderInfo(TelegramObject): """ + __slots__ = ('email', 'shipping_address', 'phone_number', 'name', '_id_attrs') + def __init__( self, name: str = None, diff --git a/telegram/payment/precheckoutquery.py b/telegram/payment/precheckoutquery.py index b4d66338a..a8f2eb293 100644 --- a/telegram/payment/precheckoutquery.py +++ b/telegram/payment/precheckoutquery.py @@ -67,6 +67,18 @@ class PreCheckoutQuery(TelegramObject): """ + __slots__ = ( + 'bot', + 'invoice_payload', + 'shipping_option_id', + 'currency', + 'order_info', + 'total_amount', + 'id', + 'from_user', + '_id_attrs', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/payment/shippingaddress.py b/telegram/payment/shippingaddress.py index fb4605255..2ea5a458e 100644 --- a/telegram/payment/shippingaddress.py +++ b/telegram/payment/shippingaddress.py @@ -49,6 +49,16 @@ class ShippingAddress(TelegramObject): """ + __slots__ = ( + 'post_code', + 'city', + '_id_attrs', + 'country_code', + 'street_line2', + 'street_line1', + 'state', + ) + def __init__( self, country_code: str, diff --git a/telegram/payment/shippingoption.py b/telegram/payment/shippingoption.py index 1be8a8871..6ddbb0bc2 100644 --- a/telegram/payment/shippingoption.py +++ b/telegram/payment/shippingoption.py @@ -46,6 +46,8 @@ class ShippingOption(TelegramObject): """ + __slots__ = ('prices', 'title', 'id', '_id_attrs') + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/payment/shippingquery.py b/telegram/payment/shippingquery.py index 198a73ecd..bcde858b6 100644 --- a/telegram/payment/shippingquery.py +++ b/telegram/payment/shippingquery.py @@ -54,6 +54,8 @@ class ShippingQuery(TelegramObject): """ + __slots__ = ('bot', 'invoice_payload', 'shipping_address', 'id', 'from_user', '_id_attrs') + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/payment/successfulpayment.py b/telegram/payment/successfulpayment.py index 2d6541873..6997ca735 100644 --- a/telegram/payment/successfulpayment.py +++ b/telegram/payment/successfulpayment.py @@ -62,6 +62,17 @@ class SuccessfulPayment(TelegramObject): """ + __slots__ = ( + 'invoice_payload', + 'shipping_option_id', + 'currency', + 'order_info', + 'telegram_payment_charge_id', + 'provider_payment_charge_id', + 'total_amount', + '_id_attrs', + ) + def __init__( self, currency: str, diff --git a/telegram/poll.py b/telegram/poll.py index 9a59bf303..9c28ce57d 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -48,6 +48,8 @@ class PollOption(TelegramObject): """ + __slots__ = ('voter_count', 'text', '_id_attrs') + def __init__(self, text: str, voter_count: int, **_kwargs: Any): self.text = text self.voter_count = voter_count @@ -78,6 +80,8 @@ class PollAnswer(TelegramObject): """ + __slots__ = ('option_ids', 'user', 'poll_id', '_id_attrs') + def __init__(self, poll_id: str, user: User, option_ids: List[int], **_kwargs: Any): self.poll_id = poll_id self.user = user @@ -146,6 +150,23 @@ class Poll(TelegramObject): """ + __slots__ = ( + 'total_voter_count', + 'allows_multiple_answers', + 'open_period', + 'options', + 'type', + 'explanation_entities', + 'is_anonymous', + 'close_date', + 'is_closed', + 'id', + 'explanation', + 'question', + 'correct_option_id', + '_id_attrs', + ) + def __init__( self, id: str, # pylint: disable=W0622 diff --git a/telegram/proximityalerttriggered.py b/telegram/proximityalerttriggered.py index fae6169d8..507fb779f 100644 --- a/telegram/proximityalerttriggered.py +++ b/telegram/proximityalerttriggered.py @@ -46,6 +46,8 @@ class ProximityAlertTriggered(TelegramObject): """ + __slots__ = ('traveler', 'distance', 'watcher', '_id_attrs') + def __init__(self, traveler: User, watcher: User, distance: int, **_kwargs: Any): self.traveler = traveler self.watcher = watcher diff --git a/telegram/replykeyboardmarkup.py b/telegram/replykeyboardmarkup.py index c41c29724..490ce338c 100644 --- a/telegram/replykeyboardmarkup.py +++ b/telegram/replykeyboardmarkup.py @@ -65,6 +65,8 @@ class ReplyKeyboardMarkup(ReplyMarkup): """ + __slots__ = ('selective', 'keyboard', 'resize_keyboard', 'one_time_keyboard', '_id_attrs') + def __init__( self, keyboard: Sequence[Sequence[Union[str, KeyboardButton]]], diff --git a/telegram/replykeyboardremove.py b/telegram/replykeyboardremove.py index 8e01b3738..5f3c255a6 100644 --- a/telegram/replykeyboardremove.py +++ b/telegram/replykeyboardremove.py @@ -55,6 +55,8 @@ class ReplyKeyboardRemove(ReplyMarkup): """ + __slots__ = ('selective', 'remove_keyboard') + def __init__(self, selective: bool = False, **_kwargs: Any): # Required self.remove_keyboard = True diff --git a/telegram/replymarkup.py b/telegram/replymarkup.py index 8c77b1e40..4f2c01d27 100644 --- a/telegram/replymarkup.py +++ b/telegram/replymarkup.py @@ -29,3 +29,5 @@ class ReplyMarkup(TelegramObject): detailed use. """ + + __slots__ = () diff --git a/telegram/update.py b/telegram/update.py index 58e9d9338..b610b35b0 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -125,6 +125,27 @@ class Update(TelegramObject): """ + __slots__ = ( + 'callback_query', + 'chosen_inline_result', + 'pre_checkout_query', + 'inline_query', + 'update_id', + 'message', + 'shipping_query', + 'poll', + 'poll_answer', + 'channel_post', + 'edited_channel_post', + 'edited_message', + '_effective_user', + '_effective_chat', + '_effective_message', + 'my_chat_member', + 'chat_member', + '_id_attrs', + ) + MESSAGE = constants.UPDATE_MESSAGE """:const:`telegram.constants.UPDATE_MESSAGE` diff --git a/telegram/user.py b/telegram/user.py index 487d8d487..ecb4a3cf1 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -96,6 +96,20 @@ class User(TelegramObject): """ + __slots__ = ( + 'is_bot', + 'can_read_all_group_messages', + 'username', + 'first_name', + 'last_name', + 'can_join_groups', + 'supports_inline_queries', + 'id', + 'bot', + 'language_code', + '_id_attrs', + ) + def __init__( self, id: int, diff --git a/telegram/userprofilephotos.py b/telegram/userprofilephotos.py index ccf6e5bf1..bd277bf1f 100644 --- a/telegram/userprofilephotos.py +++ b/telegram/userprofilephotos.py @@ -44,6 +44,8 @@ class UserProfilePhotos(TelegramObject): """ + __slots__ = ('photos', 'total_count', '_id_attrs') + def __init__(self, total_count: int, photos: List[List[PhotoSize]], **_kwargs: Any): # Required self.total_count = int(total_count) diff --git a/telegram/utils/deprecate.py b/telegram/utils/deprecate.py index 23fd1ca10..fbf604211 100644 --- a/telegram/utils/deprecate.py +++ b/telegram/utils/deprecate.py @@ -18,9 +18,28 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module facilitates the deprecation of functions.""" +import warnings + # We use our own DeprecationWarning since they are muted by default and "UserWarning" makes it # seem like it's the user that issued the warning # We name it something else so that you don't get confused when you attempt to suppress it class TelegramDeprecationWarning(Warning): """Custom warning class for deprecations in this library.""" + + __slots__ = () + + +# Function to warn users that setting custom attributes is deprecated (Use only in __setattr__!) +# Checks if a custom attribute is added by checking length of dictionary before & after +# assigning attribute. This is the fastest way to do it (I hope!). +def set_new_attribute_deprecated(self: object, key: str, value: object) -> None: + """Warns the user if they set custom attributes on PTB objects.""" + org = len(self.__dict__) + object.__setattr__(self, key, value) + new = len(self.__dict__) + if new > org: + warnings.warn( + "Setting custom attributes on objects of the PTB library is deprecated.", + TelegramDeprecationWarning, + ) diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 4e326172f..6705cc906 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -544,6 +544,8 @@ class DefaultValue(Generic[DVType]): """ + __slots__ = ('value', '__dict__') + def __init__(self, value: DVType = None): self.value = value diff --git a/telegram/utils/request.py b/telegram/utils/request.py index beb0c7b85..f2c35bfdf 100644 --- a/telegram/utils/request.py +++ b/telegram/utils/request.py @@ -70,6 +70,7 @@ from telegram.error import ( Unauthorized, ) from telegram.utils.types import JSONDict +from telegram.utils.deprecate import set_new_attribute_deprecated def _render_part(self: RequestField, name: str, value: str) -> str: # pylint: disable=W0613 @@ -111,6 +112,8 @@ class Request: """ + __slots__ = ('_connect_timeout', '_con_pool_size', '_con_pool', '__dict__') + def __init__( self, con_pool_size: int = 1, @@ -189,6 +192,9 @@ class Request: self._con_pool = mgr + def __setattr__(self, key: str, value: object) -> None: + set_new_attribute_deprecated(self, key, value) + @property def con_pool_size(self) -> int: """The size of the connection pool used.""" diff --git a/telegram/voicechat.py b/telegram/voicechat.py index 9ea91e2a2..4fb7b5398 100644 --- a/telegram/voicechat.py +++ b/telegram/voicechat.py @@ -38,6 +38,8 @@ class VoiceChatStarted(TelegramObject): .. versionadded:: 13.4 """ + __slots__ = () + def __init__(self, **_kwargs: Any): # skipcq: PTC-W0049 pass @@ -62,6 +64,8 @@ class VoiceChatEnded(TelegramObject): """ + __slots__ = ('duration', '_id_attrs') + def __init__(self, duration: int, **_kwargs: Any) -> None: self.duration = int(duration) if duration is not None else None self._id_attrs = (self.duration,) @@ -89,6 +93,8 @@ class VoiceChatParticipantsInvited(TelegramObject): """ + __slots__ = ('users', '_id_attrs') + def __init__(self, users: List[User], **_kwargs: Any) -> None: self.users = users self._id_attrs = (self.users,) @@ -134,6 +140,8 @@ class VoiceChatScheduled(TelegramObject): """ + __slots__ = ('start_date', '_id_attrs') + def __init__(self, start_date: dtm.datetime, **_kwargs: Any) -> None: self.start_date = start_date diff --git a/telegram/webhookinfo.py b/telegram/webhookinfo.py index 64c7082bf..0fc6741e4 100644 --- a/telegram/webhookinfo.py +++ b/telegram/webhookinfo.py @@ -62,6 +62,18 @@ class WebhookInfo(TelegramObject): """ + __slots__ = ( + 'allowed_updates', + 'url', + 'max_connections', + 'last_error_date', + 'ip_address', + 'last_error_message', + 'pending_update_count', + 'has_custom_certificate', + '_id_attrs', + ) + def __init__( self, url: str, diff --git a/tests/conftest.py b/tests/conftest.py index 21674eac8..f83df145b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ import datetime import functools import inspect + import os import re from collections import defaultdict @@ -83,7 +84,10 @@ def bot_info(): @pytest.fixture(scope='session') def bot(bot_info): - return make_bot(bot_info) + class DictBot(Bot): # Subclass Bot to allow monkey patching of attributes and functions, would + pass # come into effect when we __dict__ is dropped from slots + + return DictBot(bot_info['token'], private_key=PRIVATE_KEY) DEFAULT_BOTS = {} @@ -165,10 +169,12 @@ def dp(_dp): _dp.handlers = {} _dp.groups = [] _dp.error_handlers = {} - _dp.__stop_event = Event() - _dp.__exception_event = Event() - _dp.__async_queue = Queue() - _dp.__async_threads = set() + # For some reason if we setattr with the name mangled, then some tests(like async) run forever, + # due to threads not acquiring, (blocking). This adds these attributes to the __dict__. + object.__setattr__(_dp, '__stop_event', Event()) + object.__setattr__(_dp, '__exception_event', Event()) + object.__setattr__(_dp, '__async_queue', Queue()) + object.__setattr__(_dp, '__async_threads', set()) _dp.persistence = None _dp.use_context = False if _dp._Dispatcher__singleton_semaphore.acquire(blocking=0): @@ -337,6 +343,20 @@ def timezone(tzinfo): return tzinfo +@pytest.fixture() +def mro_slots(): + def _mro_slots(_class): + return [ + attr + for cls in _class.__class__.__mro__[:-1] + if hasattr(cls, '__slots__') # ABC doesn't have slots in py 3.7 and below + for attr in cls.__slots__ + if attr != '__dict__' + ] + + return _mro_slots + + def expect_bad_request(func, message, reason): """ Wrapper for testing bot functions expected to result in an :class:`telegram.error.BadRequest`. diff --git a/tests/test_animation.py b/tests/test_animation.py index 4df3f96e7..c42403de6 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import os from pathlib import Path @@ -58,6 +57,14 @@ class TestAnimation: file_size = 4127 caption = "Test *animation*" + def test_slot_behaviour(self, animation, recwarn, mro_slots): + for attr in animation.__slots__: + assert getattr(animation, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not animation.__dict__, f"got missing slot(s): {animation.__dict__}" + assert len(mro_slots(animation)) == len(set(mro_slots(animation))), "duplicate slot" + animation.custom, animation.file_name = 'should give warning', self.file_name + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, animation): assert isinstance(animation, Animation) assert isinstance(animation.file_id, str) @@ -97,6 +104,7 @@ class TestAnimation: assert message.animation.thumb.height == self.height @flaky(3, 1) + @pytest.mark.filterwarnings("ignore:.*custom attributes") def test_send_animation_custom_filename(self, bot, chat_id, animation_file, monkeypatch): def make_assertion(url, data, **kwargs): return data['animation'].filename == 'custom_filename' @@ -104,6 +112,7 @@ class TestAnimation: monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.send_animation(chat_id, animation_file, filename='custom_filename') + monkeypatch.delattr(bot.request, 'post') @flaky(3, 1) def test_get_and_download(self, bot, animation): @@ -196,6 +205,7 @@ class TestAnimation: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_animation(chat_id, file, thumb=file) assert test_flag + monkeypatch.delattr(bot, '_post') @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_audio.py b/tests/test_audio.py index 2720e8afa..924c7220f 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -59,6 +59,14 @@ class TestAudio: audio_file_id = '5a3128a4d2a04750b5b58397f3b5e812' audio_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, audio, recwarn, mro_slots): + for attr in audio.__slots__: + assert getattr(audio, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not audio.__dict__, f"got missing slot(s): {audio.__dict__}" + assert len(mro_slots(audio)) == len(set(mro_slots(audio))), "duplicate slot" + audio.custom, audio.file_name = 'should give warning', self.file_name + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, audio): # Make sure file has been uploaded. assert isinstance(audio, Audio) @@ -217,6 +225,7 @@ class TestAudio: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_audio(chat_id, file, thumb=file) assert test_flag + monkeypatch.delattr(bot, '_post') def test_de_json(self, bot, audio): json_dict = { diff --git a/tests/test_bot.py b/tests/test_bot.py index ca2701f38..7e0b5974f 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -103,7 +103,28 @@ xfail = pytest.mark.xfail( ) +@pytest.fixture(scope='function') +def inst(request, bot_info, default_bot): + return Bot(bot_info['token']) if request.param == 'bot' else default_bot + + class TestBot: + @pytest.mark.parametrize('inst', ['bot', "default_bot"], indirect=True) + def test_slot_behaviour(self, inst, recwarn, mro_slots): + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slots: {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.base_url = 'should give warning', inst.base_url + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + class CustomBot(Bot): + pass # Tests that setting custom attributes of Bot subclass doesn't raise warning + + a = CustomBot(inst.token) + a.my_custom = 'no error!' + assert len(recwarn) == 1 + @pytest.mark.parametrize( 'token', argvalues=[ @@ -627,6 +648,7 @@ class TestBot: bot.send_chat_action(chat_id, 'unknown action') # TODO: Needs improvement. We need incoming inline query to test answer. + @pytest.mark.filterwarnings("ignore:.*custom attributes") def test_answer_inline_query(self, monkeypatch, bot): # For now just test that our internals pass the correct data def test(url, data, *args, **kwargs): @@ -668,6 +690,7 @@ class TestBot: switch_pm_text='switch pm', switch_pm_parameter='start_pm', ) + monkeypatch.delattr(bot.request, 'post') def test_answer_inline_query_no_default_parse_mode(self, monkeypatch, bot): def test(url, data, *args, **kwargs): @@ -875,8 +898,10 @@ class TestBot: resulting_path = bot.get_file('file_id').file_path assert bot.token not in resulting_path assert resulting_path == path + monkeypatch.delattr(bot, '_post') # TODO: Needs improvement. No feasible way to test until bots can add members. + @pytest.mark.filterwarnings("ignore:.*custom attributes") def test_kick_chat_member(self, monkeypatch, bot): def test(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 @@ -892,6 +917,7 @@ class TestBot: assert bot.kick_chat_member(2, 32, until_date=until) assert bot.kick_chat_member(2, 32, until_date=1577887200) assert bot.kick_chat_member(2, 32, revoke_messages=True) + monkeypatch.delattr(bot.request, 'post') def test_kick_chat_member_default_tz(self, monkeypatch, tz_bot): until = dtm.datetime(2020, 1, 11, 16, 13) diff --git a/tests/test_botcommand.py b/tests/test_botcommand.py index ddf9363fb..1b750d996 100644 --- a/tests/test_botcommand.py +++ b/tests/test_botcommand.py @@ -31,6 +31,14 @@ class TestBotCommand: command = 'start' description = 'A command' + def test_slot_behaviour(self, bot_command, recwarn, mro_slots): + for attr in bot_command.__slots__: + assert getattr(bot_command, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not bot_command.__dict__, f"got missing slot(s): {bot_command.__dict__}" + assert len(mro_slots(bot_command)) == len(set(mro_slots(bot_command))), "duplicate slot" + bot_command.custom, bot_command.command = 'should give warning', self.command + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = {'command': self.command, 'description': self.description} bot_command = BotCommand.de_json(json_dict, bot) diff --git a/tests/test_callbackcontext.py b/tests/test_callbackcontext.py index 8018b0ce0..cb9bc8034 100644 --- a/tests/test_callbackcontext.py +++ b/tests/test_callbackcontext.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. + import pytest from telegram import Update, Message, Chat, User, TelegramError @@ -23,6 +24,15 @@ from telegram.ext import CallbackContext class TestCallbackContext: + def test_slot_behaviour(self, cdp, recwarn, mro_slots): + c = CallbackContext(cdp) + for attr in c.__slots__: + assert getattr(c, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not c.__dict__, f"got missing slot(s): {c.__dict__}" + assert len(mro_slots(c)) == len(set(mro_slots(c))), "duplicate slot" + c.args = c.args + assert len(recwarn) == 0, recwarn.list + def test_non_context_dp(self, dp): with pytest.raises(ValueError): CallbackContext(dp) diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 7ea3ca857..56aede670 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -50,6 +50,14 @@ class TestCallbackQuery: inline_message_id = 'inline_message_id' game_short_name = 'the_game' + def test_slot_behaviour(self, callback_query, recwarn, mro_slots): + for attr in callback_query.__slots__: + assert getattr(callback_query, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not callback_query.__dict__, f"got missing slot(s): {callback_query.__dict__}" + assert len(mro_slots(callback_query)) == len(set(mro_slots(callback_query))), "same slot" + callback_query.custom, callback_query.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @staticmethod def skip_params(callback_query: CallbackQuery): if callback_query.inline_message_id: diff --git a/tests/test_callbackqueryhandler.py b/tests/test_callbackqueryhandler.py index a6ccbb767..064279f8e 100644 --- a/tests/test_callbackqueryhandler.py +++ b/tests/test_callbackqueryhandler.py @@ -72,6 +72,15 @@ def callback_query(bot): class TestCallbackQueryHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + handler = CallbackQueryHandler(self.callback_data_1, pass_user_data=True) + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_chat.py b/tests/test_chat.py index 86df95fce..0f87251ca 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -63,6 +63,14 @@ class TestChat: linked_chat_id = 11880 location = ChatLocation(Location(123, 456), 'Barbie World') + def test_slot_behaviour(self, chat, recwarn, mro_slots): + for attr in chat.__slots__: + assert getattr(chat, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not chat.__dict__, f"got missing slot(s): {chat.__dict__}" + assert len(mro_slots(chat)) == len(set(mro_slots(chat))), "duplicate slot" + chat.custom, chat.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'id': self.id_, diff --git a/tests/test_chataction.py b/tests/test_chataction.py new file mode 100644 index 000000000..619039928 --- /dev/null +++ b/tests/test_chataction.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +from telegram import ChatAction + + +def test_slot_behaviour(recwarn, mro_slots): + action = ChatAction() + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py index 5661deab0..8b4fcadfd 100644 --- a/tests/test_chatinvitelink.py +++ b/tests/test_chatinvitelink.py @@ -49,6 +49,14 @@ class TestChatInviteLink: expire_date = datetime.datetime.utcnow() member_limit = 42 + def test_slot_behaviour(self, recwarn, mro_slots, invite_link): + for attr in invite_link.__slots__: + assert getattr(invite_link, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not invite_link.__dict__, f"got missing slot(s): {invite_link.__dict__}" + assert len(mro_slots(invite_link)) == len(set(mro_slots(invite_link))), "duplicate slot" + invite_link.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json_required_args(self, bot, creator): json_dict = { 'invite_link': self.link, diff --git a/tests/test_chatlocation.py b/tests/test_chatlocation.py index ce716e4f2..1facfde2e 100644 --- a/tests/test_chatlocation.py +++ b/tests/test_chatlocation.py @@ -31,6 +31,15 @@ class TestChatLocation: location = Location(123, 456) address = 'The Shire' + def test_slot_behaviour(self, chat_location, recwarn, mro_slots): + inst = chat_location + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.address = 'should give warning', self.address + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'location': self.location.to_dict(), diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index e6e34d0e7..967eb3df1 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -37,6 +37,14 @@ def chat_member(user): class TestChatMember: status = ChatMember.CREATOR + def test_slot_behaviour(self, chat_member, recwarn, mro_slots): + for attr in chat_member.__slots__: + assert getattr(chat_member, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not chat_member.__dict__, f"got missing slot(s): {chat_member.__dict__}" + assert len(mro_slots(chat_member)) == len(set(mro_slots(chat_member))), "duplicate slot" + chat_member.custom, chat_member.status = 'should give warning', self.status + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json_required_args(self, bot, user): json_dict = {'user': user.to_dict(), 'status': self.status} diff --git a/tests/test_chatmemberhandler.py b/tests/test_chatmemberhandler.py index 85459ede6..1fc75c71d 100644 --- a/tests/test_chatmemberhandler.py +++ b/tests/test_chatmemberhandler.py @@ -88,6 +88,15 @@ def chat_member(bot, chat_member_updated): class TestChatMemberHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + action = ChatMemberHandler(self.callback_basic) + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index c01bc4d8d..d90e83761 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -65,6 +65,15 @@ class TestChatMemberUpdated: old_status = ChatMember.MEMBER new_status = ChatMember.ADMINISTRATOR + def test_slot_behaviour(self, recwarn, mro_slots, chat_member_updated): + action = chat_member_updated + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json_required_args(self, bot, user, chat, old_chat_member, new_chat_member, time): json_dict = { 'chat': chat.to_dict(), diff --git a/tests/test_chatpermissions.py b/tests/test_chatpermissions.py index 0ae5614e2..c47ae6669 100644 --- a/tests/test_chatpermissions.py +++ b/tests/test_chatpermissions.py @@ -46,6 +46,15 @@ class TestChatPermissions: can_invite_users = None can_pin_messages = None + def test_slot_behaviour(self, chat_permissions, recwarn, mro_slots): + inst = chat_permissions + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.can_send_polls = 'should give warning', self.can_send_polls + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'can_send_messages': self.can_send_messages, diff --git a/tests/test_chatphoto.py b/tests/test_chatphoto.py index a0e936874..3676b0e1b 100644 --- a/tests/test_chatphoto.py +++ b/tests/test_chatphoto.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import os import pytest from flaky import flaky @@ -52,6 +51,14 @@ class TestChatPhoto: chatphoto_big_file_unique_id = 'bigadc3145fd2e84d95b64d68eaa22aa33e' chatphoto_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.jpg' + def test_slot_behaviour(self, chat_photo, recwarn, mro_slots): + for attr in chat_photo.__slots__: + assert getattr(chat_photo, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not chat_photo.__dict__, f"got missing slot(s): {chat_photo.__dict__}" + assert len(mro_slots(chat_photo)) == len(set(mro_slots(chat_photo))), "duplicate slot" + chat_photo.custom, chat_photo.big_file_id = 'gives warning', self.chatphoto_big_file_id + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @flaky(3, 1) def test_send_all_args(self, bot, super_group_id, chatphoto_file, chat_photo, thumb_file): def func(): diff --git a/tests/test_choseninlineresult.py b/tests/test_choseninlineresult.py index 50037b6e4..a6a797ce0 100644 --- a/tests/test_choseninlineresult.py +++ b/tests/test_choseninlineresult.py @@ -36,6 +36,15 @@ class TestChosenInlineResult: result_id = 'result id' query = 'query text' + def test_slot_behaviour(self, chosen_inline_result, recwarn, mro_slots): + inst = chosen_inline_result + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.result_id = 'should give warning', self.result_id + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json_required(self, bot, user): json_dict = {'result_id': self.result_id, 'from': user.to_dict(), 'query': self.query} result = ChosenInlineResult.de_json(json_dict, bot) diff --git a/tests/test_choseninlineresulthandler.py b/tests/test_choseninlineresulthandler.py index c286a22c1..1803a291b 100644 --- a/tests/test_choseninlineresulthandler.py +++ b/tests/test_choseninlineresulthandler.py @@ -81,6 +81,15 @@ class TestChosenInlineResultHandler: def reset(self): self.test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + handler = ChosenInlineResultHandler(self.callback_basic) + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def callback_basic(self, bot, update): test_bot = isinstance(bot, Bot) test_update = isinstance(update, Update) diff --git a/tests/test_commandhandler.py b/tests/test_commandhandler.py index 1539a7320..6c6262545 100644 --- a/tests/test_commandhandler.py +++ b/tests/test_commandhandler.py @@ -142,6 +142,15 @@ class BaseTest: class TestCommandHandler(BaseTest): CMD = '/test' + def test_slot_behaviour(self, recwarn, mro_slots): + handler = self.make_default_handler() + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler.command = 'should give warning', self.CMD + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(scope='class') def command(self): return self.CMD @@ -296,6 +305,15 @@ class TestPrefixHandler(BaseTest): COMMANDS = ['help', 'test'] COMBINATIONS = list(combinations(PREFIXES, COMMANDS)) + def test_slot_behaviour(self, mro_slots, recwarn): + handler = self.make_default_handler() + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler.command = 'should give warning', self.COMMANDS + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(scope='class', params=PREFIXES) def prefix(self, request): return request.param diff --git a/tests/test_contact.py b/tests/test_contact.py index a70c0f778..4ad6b699a 100644 --- a/tests/test_contact.py +++ b/tests/test_contact.py @@ -40,6 +40,14 @@ class TestContact: last_name = 'Toledo' user_id = 23 + def test_slot_behaviour(self, contact, recwarn, mro_slots): + for attr in contact.__slots__: + assert getattr(contact, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not contact.__dict__, f"got missing slot(s): {contact.__dict__}" + assert len(mro_slots(contact)) == len(set(mro_slots(contact))), "duplicate slot" + contact.custom, contact.first_name = 'should give warning', self.first_name + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json_required(self, bot): json_dict = {'phone_number': self.phone_number, 'first_name': self.first_name} contact = Contact.de_json(json_dict, bot) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 2e5da4ca2..eaee2afa3 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -94,6 +94,17 @@ class TestConversationHandler: raise_dp_handler_stop = False test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + handler = ConversationHandler(self.entry_points, self.states, self.fallbacks) + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler._persistence = 'should give warning', handler._persistence + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), [ + w.message for w in recwarn.list + ] + # Test related @pytest.fixture(autouse=True) def reset(self): diff --git a/tests/test_defaults.py b/tests/test_defaults.py index b12029cc8..99a85bae4 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -24,6 +24,15 @@ from telegram import User class TestDefault: + def test_slot_behaviour(self, recwarn, mro_slots): + a = Defaults(parse_mode='HTML', quote=True) + for attr in a.__slots__: + assert getattr(a, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not a.__dict__, f"got missing slot(s): {a.__dict__}" + assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot" + a.custom, a._parse_mode = 'should give warning', a._parse_mode + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_data_assignment(self, cdp): defaults = Defaults() diff --git a/tests/test_dice.py b/tests/test_dice.py index 88a5d0a76..cced04001 100644 --- a/tests/test_dice.py +++ b/tests/test_dice.py @@ -30,6 +30,14 @@ def dice(request): class TestDice: value = 4 + def test_slot_behaviour(self, dice, recwarn, mro_slots): + for attr in dice.__slots__: + assert getattr(dice, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not dice.__dict__, f"got missing slot(s): {dice.__dict__}" + assert len(mro_slots(dice)) == len(set(mro_slots(dice))), "duplicate slot" + dice.custom, dice.value = 'should give warning', self.value + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) def test_de_json(self, bot, emoji): json_dict = {'value': self.value, 'emoji': emoji} diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 43b1a2dba..672250999 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -52,6 +52,25 @@ class TestDispatcher: received = None count = 0 + def test_slot_behaviour(self, dp2, recwarn, mro_slots): + for at in dp2.__slots__: + at = f"_Dispatcher{at}" if at.startswith('__') and not at.endswith('__') else at + assert getattr(dp2, at, 'err') != 'err', f"got extra slot '{at}'" + assert not dp2.__dict__, f"got missing slot(s): {dp2.__dict__}" + assert len(mro_slots(dp2)) == len(set(mro_slots(dp2))), "duplicate slot" + dp2.custom, dp2.running = 'should give warning', dp2.running + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + class CustomDispatcher(Dispatcher): + pass # Tests that setting custom attrs of Dispatcher subclass doesn't raise warning + + a = CustomDispatcher(None, None) + a.my_custom = 'no error!' + assert len(recwarn) == 1 + + dp2.__setattr__('__test', 'mangled success') + assert getattr(dp2, '_Dispatcher__test', 'e') == 'mangled success', "mangling failed" + @pytest.fixture(autouse=True, name='reset') def reset_fixture(self): self.reset() @@ -684,7 +703,6 @@ class TestDispatcher: def test_sensible_worker_thread_names(self, dp2): thread_names = [thread.name for thread in dp2._Dispatcher__async_threads] - print(thread_names) for thread_name in thread_names: assert thread_name.startswith(f"Bot:{dp2.bot.id}:worker:") diff --git a/tests/test_document.py b/tests/test_document.py index eb2867053..fa00faf6e 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -53,6 +53,14 @@ class TestDocument: document_file_id = '5a3128a4d2a04750b5b58397f3b5e812' document_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, document, recwarn, mro_slots): + for attr in document.__slots__: + assert getattr(document, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not document.__dict__, f"got missing slot(s): {document.__dict__}" + assert len(mro_slots(document)) == len(set(mro_slots(document))), "duplicate slot" + document.custom, document.file_name = 'should give warning', self.file_name + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), f"{recwarn}" + def test_creation(self, document): assert isinstance(document, Document) assert isinstance(document.file_id, str) @@ -242,6 +250,7 @@ class TestDocument: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_document(chat_id, file, thumb=file) assert test_flag + monkeypatch.delattr(bot, '_post') def test_de_json(self, bot, document): json_dict = { diff --git a/tests/test_encryptedcredentials.py b/tests/test_encryptedcredentials.py index 3f3ca7f47..085f82f12 100644 --- a/tests/test_encryptedcredentials.py +++ b/tests/test_encryptedcredentials.py @@ -36,6 +36,15 @@ class TestEncryptedCredentials: hash = 'hash' secret = 'secret' + def test_slot_behaviour(self, encrypted_credentials, recwarn, mro_slots): + inst = encrypted_credentials + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.data = 'should give warning', self.data + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, encrypted_credentials): assert encrypted_credentials.data == self.data assert encrypted_credentials.hash == self.hash diff --git a/tests/test_encryptedpassportelement.py b/tests/test_encryptedpassportelement.py index 79b1817cf..0505c5ad0 100644 --- a/tests/test_encryptedpassportelement.py +++ b/tests/test_encryptedpassportelement.py @@ -46,6 +46,15 @@ class TestEncryptedPassportElement: reverse_side = PassportFile('file_id', 50, 0) selfie = PassportFile('file_id', 50, 0) + def test_slot_behaviour(self, encrypted_passport_element, recwarn, mro_slots): + inst = encrypted_passport_element + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.phone_number = 'should give warning', self.phone_number + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, encrypted_passport_element): assert encrypted_passport_element.type == self.type_ assert encrypted_passport_element.data == self.data diff --git a/tests/test_file.py b/tests/test_file.py index d55b4ddf6..953be29e9 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -57,6 +57,14 @@ class TestFile: file_size = 28232 file_content = 'Saint-Saëns'.encode() # Intentionally contains unicode chars. + def test_slot_behaviour(self, file, recwarn, mro_slots): + for attr in file.__slots__: + assert getattr(file, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not file.__dict__, f"got missing slot(s): {file.__dict__}" + assert len(mro_slots(file)) == len(set(mro_slots(file))), "duplicate slot" + file.custom, file.file_id = 'should give warning', self.file_id + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'file_id': self.file_id, diff --git a/tests/test_filters.py b/tests/test_filters.py index b3e741e00..efebc477f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -22,6 +22,8 @@ import pytest from telegram import Message, User, Chat, MessageEntity, Document, Update, Dice from telegram.ext import Filters, BaseFilter, MessageFilter, UpdateFilter +from sys import version_info as py_ver +import inspect import re from telegram.utils.deprecate import TelegramDeprecationWarning @@ -59,6 +61,67 @@ def base_class(request): class TestFilters: + def test_all_filters_slot_behaviour(self, recwarn, mro_slots): + """ + Use depth first search to get all nested filters, and instantiate them (which need it) with + the correct number of arguments, then test each filter separately. Also tests setting + custom attributes on custom filters. + """ + # The total no. of filters excluding filters defined in __all__ is about 70 as of 16/2/21. + # Gather all the filters to test using DFS- + visited = [] + classes = inspect.getmembers(Filters, predicate=inspect.isclass) # List[Tuple[str, type]] + stack = classes.copy() + while stack: + cls = stack[-1][-1] # get last element and its class + for inner_cls in inspect.getmembers( + cls, # Get inner filters + lambda a: inspect.isclass(a) and not issubclass(a, cls.__class__), + ): + if inner_cls[1] not in visited: + stack.append(inner_cls) + visited.append(inner_cls[1]) + classes.append(inner_cls) + break + else: + stack.pop() + + # Now start the actual testing + for name, cls in classes: + # Can't instantiate abstract classes without overriding methods, so skip them for now + if inspect.isabstract(cls) or name in {'__class__', '__base__'}: + continue + + assert '__slots__' in cls.__dict__, f"Filter {name!r} doesn't have __slots__" + # get no. of args minus the 'self' argument + args = len(inspect.signature(cls.__init__).parameters) - 1 + if cls.__base__.__name__ == '_ChatUserBaseFilter': # Special case, only 1 arg needed + inst = cls('1') + else: + inst = cls() if args < 1 else cls(*['blah'] * args) # unpack variable no. of args + + for attr in cls.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}' for {name}" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__} for {name}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), f"same slot in {name}" + + with pytest.warns(TelegramDeprecationWarning, match='custom attributes') as warn: + inst.custom = 'should give warning' + if not warn: + pytest.fail(f"Filter {name!r} didn't warn when setting custom attr") + + assert '__dict__' not in BaseFilter.__slots__ if py_ver < (3, 7) else True, 'dict in abc' + + class CustomFilter(MessageFilter): + def filter(self, message: Message): + pass + + with pytest.warns(None): + CustomFilter().custom = 'allowed' # Test setting custom attr to custom filters + + with pytest.warns(TelegramDeprecationWarning, match='custom attributes'): + Filters().custom = 'raise warning' + def test_filters_all(self, update): assert Filters.all(update) diff --git a/tests/test_forcereply.py b/tests/test_forcereply.py index 988d7669d..80b2d5590 100644 --- a/tests/test_forcereply.py +++ b/tests/test_forcereply.py @@ -32,6 +32,14 @@ class TestForceReply: force_reply = True selective = True + def test_slot_behaviour(self, force_reply, recwarn, mro_slots): + for attr in force_reply.__slots__: + assert getattr(force_reply, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not force_reply.__dict__, f"got missing slot(s): {force_reply.__dict__}" + assert len(mro_slots(force_reply)) == len(set(mro_slots(force_reply))), "duplicate slot" + force_reply.custom, force_reply.force_reply = 'should give warning', self.force_reply + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @flaky(3, 1) def test_send_message_with_force_reply(self, bot, chat_id, force_reply): message = bot.send_message(chat_id, 'text', reply_markup=force_reply) diff --git a/tests/test_game.py b/tests/test_game.py index 357bbe9e2..8207cd708 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -45,6 +45,14 @@ class TestGame: text_entities = [MessageEntity(13, 17, MessageEntity.URL)] animation = Animation('blah', 'unique_id', 320, 180, 1) + def test_slot_behaviour(self, game, recwarn, mro_slots): + for attr in game.__slots__: + assert getattr(game, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not game.__dict__, f"got missing slot(s): {game.__dict__}" + assert len(mro_slots(game)) == len(set(mro_slots(game))), "duplicate slot" + game.custom, game.title = 'should give warning', self.title + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json_required(self, bot): json_dict = { 'title': self.title, diff --git a/tests/test_gamehighscore.py b/tests/test_gamehighscore.py index 8605a14d0..166e22cf6 100644 --- a/tests/test_gamehighscore.py +++ b/tests/test_gamehighscore.py @@ -34,6 +34,14 @@ class TestGameHighScore: user = User(2, 'test user', False) score = 42 + def test_slot_behaviour(self, game_highscore, recwarn, mro_slots): + for attr in game_highscore.__slots__: + assert getattr(game_highscore, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not game_highscore.__dict__, f"got missing slot(s): {game_highscore.__dict__}" + assert len(mro_slots(game_highscore)) == len(set(mro_slots(game_highscore))), "same slot" + game_highscore.custom, game_highscore.position = 'should give warning', self.position + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = {'position': self.position, 'user': self.user.to_dict(), 'score': self.score} highscore = GameHighScore.de_json(json_dict, bot) diff --git a/tests/test_handler.py b/tests/test_handler.py new file mode 100644 index 000000000..b4a43c10f --- /dev/null +++ b/tests/test_handler.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +from sys import version_info as py_ver + +from telegram.ext import Handler + + +class TestHandler: + def test_slot_behaviour(self, recwarn, mro_slots): + class SubclassHandler(Handler): + __slots__ = () + + def __init__(self): + super().__init__(lambda x: None) + + def check_update(self, update: object): + pass + + inst = SubclassHandler() + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + assert '__dict__' not in Handler.__slots__ if py_ver < (3, 7) else True, 'dict in abc' + inst.custom = 'should not give warning' + assert len(recwarn) == 0, recwarn.list diff --git a/tests/test_inlinekeyboardbutton.py b/tests/test_inlinekeyboardbutton.py index fcbbc1175..b21fdbf57 100644 --- a/tests/test_inlinekeyboardbutton.py +++ b/tests/test_inlinekeyboardbutton.py @@ -46,6 +46,15 @@ class TestInlineKeyboardButton: pay = 'pay' login_url = LoginUrl("http://google.com") + def test_slot_behaviour(self, inline_keyboard_button, recwarn, mro_slots): + inst = inline_keyboard_button + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.text = 'should give warning', self.text + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_keyboard_button): assert inline_keyboard_button.text == self.text assert inline_keyboard_button.url == self.url diff --git a/tests/test_inlinekeyboardmarkup.py b/tests/test_inlinekeyboardmarkup.py index dcd983ecd..719adaa4c 100644 --- a/tests/test_inlinekeyboardmarkup.py +++ b/tests/test_inlinekeyboardmarkup.py @@ -36,6 +36,15 @@ class TestInlineKeyboardMarkup: ] ] + def test_slot_behaviour(self, inline_keyboard_markup, recwarn, mro_slots): + inst = inline_keyboard_markup + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.inline_keyboard = 'should give warning', self.inline_keyboard + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @flaky(3, 1) def test_send_message_with_inline_keyboard_markup(self, bot, chat_id, inline_keyboard_markup): message = bot.send_message( diff --git a/tests/test_inlinequery.py b/tests/test_inlinequery.py index e8f4625fe..3e80b27c5 100644 --- a/tests/test_inlinequery.py +++ b/tests/test_inlinequery.py @@ -44,6 +44,14 @@ class TestInlineQuery: location = Location(8.8, 53.1) chat_type = Chat.SENDER + def test_slot_behaviour(self, inline_query, recwarn, mro_slots): + for attr in inline_query.__slots__: + assert getattr(inline_query, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inline_query.__dict__, f"got missing slot(s): {inline_query.__dict__}" + assert len(mro_slots(inline_query)) == len(set(mro_slots(inline_query))), "duplicate slot" + inline_query.custom, inline_query.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'id': self.id_, diff --git a/tests/test_inlinequeryhandler.py b/tests/test_inlinequeryhandler.py index 0de652059..4688a8004 100644 --- a/tests/test_inlinequeryhandler.py +++ b/tests/test_inlinequeryhandler.py @@ -81,9 +81,18 @@ def inline_query(bot): ) -class TestCallbackQueryHandler: +class TestInlineQueryHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + handler = InlineQueryHandler(self.callback_context) + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_inlinequeryresultarticle.py b/tests/test_inlinequeryresultarticle.py index 4fc6a4fd9..a5a383d1d 100644 --- a/tests/test_inlinequeryresultarticle.py +++ b/tests/test_inlinequeryresultarticle.py @@ -57,6 +57,15 @@ class TestInlineQueryResultArticle: thumb_height = 10 thumb_width = 15 + def test_slot_behaviour(self, inline_query_result_article, mro_slots, recwarn): + inst = inline_query_result_article + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_article): assert inline_query_result_article.type == self.type_ assert inline_query_result_article.id == self.id_ diff --git a/tests/test_inlinequeryresultaudio.py b/tests/test_inlinequeryresultaudio.py index 39d0f346d..5071a49a1 100644 --- a/tests/test_inlinequeryresultaudio.py +++ b/tests/test_inlinequeryresultaudio.py @@ -58,6 +58,15 @@ class TestInlineQueryResultAudio: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_audio, mro_slots, recwarn): + inst = inline_query_result_audio + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_audio): assert inline_query_result_audio.type == self.type_ assert inline_query_result_audio.id == self.id_ diff --git a/tests/test_inlinequeryresultcachedaudio.py b/tests/test_inlinequeryresultcachedaudio.py index 681a5447d..33ee9b858 100644 --- a/tests/test_inlinequeryresultcachedaudio.py +++ b/tests/test_inlinequeryresultcachedaudio.py @@ -52,6 +52,15 @@ class TestInlineQueryResultCachedAudio: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_audio, mro_slots, recwarn): + inst = inline_query_result_cached_audio + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_audio): assert inline_query_result_cached_audio.type == self.type_ assert inline_query_result_cached_audio.id == self.id_ diff --git a/tests/test_inlinequeryresultcacheddocument.py b/tests/test_inlinequeryresultcacheddocument.py index 46653bc15..a25d089df 100644 --- a/tests/test_inlinequeryresultcacheddocument.py +++ b/tests/test_inlinequeryresultcacheddocument.py @@ -56,6 +56,15 @@ class TestInlineQueryResultCachedDocument: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_document, mro_slots, recwarn): + inst = inline_query_result_cached_document + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_document): assert inline_query_result_cached_document.id == self.id_ assert inline_query_result_cached_document.type == self.type_ diff --git a/tests/test_inlinequeryresultcachedgif.py b/tests/test_inlinequeryresultcachedgif.py index 67923c544..83bf386dd 100644 --- a/tests/test_inlinequeryresultcachedgif.py +++ b/tests/test_inlinequeryresultcachedgif.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -54,6 +53,15 @@ class TestInlineQueryResultCachedGif: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_gif, recwarn, mro_slots): + inst = inline_query_result_cached_gif + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_gif): assert inline_query_result_cached_gif.type == self.type_ assert inline_query_result_cached_gif.id == self.id_ diff --git a/tests/test_inlinequeryresultcachedmpeg4gif.py b/tests/test_inlinequeryresultcachedmpeg4gif.py index 3f7423161..edd485388 100644 --- a/tests/test_inlinequeryresultcachedmpeg4gif.py +++ b/tests/test_inlinequeryresultcachedmpeg4gif.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -54,6 +53,15 @@ class TestInlineQueryResultCachedMpeg4Gif: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_mpeg4_gif, mro_slots, recwarn): + inst = inline_query_result_cached_mpeg4_gif + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_mpeg4_gif): assert inline_query_result_cached_mpeg4_gif.type == self.type_ assert inline_query_result_cached_mpeg4_gif.id == self.id_ diff --git a/tests/test_inlinequeryresultcachedphoto.py b/tests/test_inlinequeryresultcachedphoto.py index 23dde5616..30f6b6c04 100644 --- a/tests/test_inlinequeryresultcachedphoto.py +++ b/tests/test_inlinequeryresultcachedphoto.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -56,6 +55,15 @@ class TestInlineQueryResultCachedPhoto: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_photo, recwarn, mro_slots): + inst = inline_query_result_cached_photo + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_photo): assert inline_query_result_cached_photo.type == self.type_ assert inline_query_result_cached_photo.id == self.id_ diff --git a/tests/test_inlinequeryresultcachedsticker.py b/tests/test_inlinequeryresultcachedsticker.py index b139ca0d0..42615fc66 100644 --- a/tests/test_inlinequeryresultcachedsticker.py +++ b/tests/test_inlinequeryresultcachedsticker.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -45,6 +44,15 @@ class TestInlineQueryResultCachedSticker: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_sticker, mro_slots, recwarn): + inst = inline_query_result_cached_sticker + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_sticker): assert inline_query_result_cached_sticker.type == self.type_ assert inline_query_result_cached_sticker.id == self.id_ diff --git a/tests/test_inlinequeryresultcachedvideo.py b/tests/test_inlinequeryresultcachedvideo.py index b853cd176..7a933e279 100644 --- a/tests/test_inlinequeryresultcachedvideo.py +++ b/tests/test_inlinequeryresultcachedvideo.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -56,6 +55,15 @@ class TestInlineQueryResultCachedVideo: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_video, recwarn, mro_slots): + inst = inline_query_result_cached_video + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_video): assert inline_query_result_cached_video.type == self.type_ assert inline_query_result_cached_video.id == self.id_ diff --git a/tests/test_inlinequeryresultcachedvoice.py b/tests/test_inlinequeryresultcachedvoice.py index 7dab8b8b4..a87239bd9 100644 --- a/tests/test_inlinequeryresultcachedvoice.py +++ b/tests/test_inlinequeryresultcachedvoice.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -54,6 +53,15 @@ class TestInlineQueryResultCachedVoice: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_cached_voice, recwarn, mro_slots): + inst = inline_query_result_cached_voice + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_cached_voice): assert inline_query_result_cached_voice.type == self.type_ assert inline_query_result_cached_voice.id == self.id_ diff --git a/tests/test_inlinequeryresultcontact.py b/tests/test_inlinequeryresultcontact.py index 0ac6780c5..c8f74e2b0 100644 --- a/tests/test_inlinequeryresultcontact.py +++ b/tests/test_inlinequeryresultcontact.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -55,6 +54,15 @@ class TestInlineQueryResultContact: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_contact, mro_slots, recwarn): + inst = inline_query_result_contact + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_contact): assert inline_query_result_contact.id == self.id_ assert inline_query_result_contact.type == self.type_ diff --git a/tests/test_inlinequeryresultdocument.py b/tests/test_inlinequeryresultdocument.py index f527a23db..983ddbab8 100644 --- a/tests/test_inlinequeryresultdocument.py +++ b/tests/test_inlinequeryresultdocument.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -64,6 +63,15 @@ class TestInlineQueryResultDocument: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_document, recwarn, mro_slots): + inst = inline_query_result_document + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_document): assert inline_query_result_document.id == self.id_ assert inline_query_result_document.type == self.type_ diff --git a/tests/test_inlinequeryresultgame.py b/tests/test_inlinequeryresultgame.py index aa8c2d69c..11fe95280 100644 --- a/tests/test_inlinequeryresultgame.py +++ b/tests/test_inlinequeryresultgame.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -42,6 +41,15 @@ class TestInlineQueryResultGame: game_short_name = 'game short name' reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_game, mro_slots, recwarn): + inst = inline_query_result_game + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_game): assert inline_query_result_game.type == self.type_ assert inline_query_result_game.id == self.id_ diff --git a/tests/test_inlinequeryresultgif.py b/tests/test_inlinequeryresultgif.py index 4137daddb..a5e251685 100644 --- a/tests/test_inlinequeryresultgif.py +++ b/tests/test_inlinequeryresultgif.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -64,6 +63,15 @@ class TestInlineQueryResultGif: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_gif, recwarn, mro_slots): + inst = inline_query_result_gif + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_gif): assert inline_query_result_gif.type == self.type_ assert inline_query_result_gif.id == self.id_ diff --git a/tests/test_inlinequeryresultlocation.py b/tests/test_inlinequeryresultlocation.py index 125d3ee78..5b4142eee 100644 --- a/tests/test_inlinequeryresultlocation.py +++ b/tests/test_inlinequeryresultlocation.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -63,6 +62,15 @@ class TestInlineQueryResultLocation: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_location, mro_slots, recwarn): + inst = inline_query_result_location + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_location): assert inline_query_result_location.id == self.id_ assert inline_query_result_location.type == self.type_ diff --git a/tests/test_inlinequeryresultmpeg4gif.py b/tests/test_inlinequeryresultmpeg4gif.py index 43290753f..cd5d2ec3b 100644 --- a/tests/test_inlinequeryresultmpeg4gif.py +++ b/tests/test_inlinequeryresultmpeg4gif.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -64,6 +63,15 @@ class TestInlineQueryResultMpeg4Gif: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_mpeg4_gif, recwarn, mro_slots): + inst = inline_query_result_mpeg4_gif + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_mpeg4_gif): assert inline_query_result_mpeg4_gif.type == self.type_ assert inline_query_result_mpeg4_gif.id == self.id_ diff --git a/tests/test_inlinequeryresultphoto.py b/tests/test_inlinequeryresultphoto.py index 5f5ae4fd2..5fd21bd63 100644 --- a/tests/test_inlinequeryresultphoto.py +++ b/tests/test_inlinequeryresultphoto.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -63,6 +62,15 @@ class TestInlineQueryResultPhoto: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_photo, recwarn, mro_slots): + inst = inline_query_result_photo + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_photo): assert inline_query_result_photo.type == self.type_ assert inline_query_result_photo.id == self.id_ diff --git a/tests/test_inlinequeryresultvenue.py b/tests/test_inlinequeryresultvenue.py index 1a26bac52..b61446570 100644 --- a/tests/test_inlinequeryresultvenue.py +++ b/tests/test_inlinequeryresultvenue.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -65,6 +64,15 @@ class TestInlineQueryResultVenue: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_venue, mro_slots, recwarn): + inst = inline_query_result_venue + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_venue): assert inline_query_result_venue.id == self.id_ assert inline_query_result_venue.type == self.type_ diff --git a/tests/test_inlinequeryresultvideo.py b/tests/test_inlinequeryresultvideo.py index 9f7381140..5e9442a1c 100644 --- a/tests/test_inlinequeryresultvideo.py +++ b/tests/test_inlinequeryresultvideo.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -66,6 +65,15 @@ class TestInlineQueryResultVideo: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_video, recwarn, mro_slots): + inst = inline_query_result_video + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_video): assert inline_query_result_video.type == self.type_ assert inline_query_result_video.id == self.id_ diff --git a/tests/test_inlinequeryresultvoice.py b/tests/test_inlinequeryresultvoice.py index 533b5e87d..ae86a48fb 100644 --- a/tests/test_inlinequeryresultvoice.py +++ b/tests/test_inlinequeryresultvoice.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ( @@ -57,6 +56,15 @@ class TestInlineQueryResultVoice: input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) + def test_slot_behaviour(self, inline_query_result_voice, mro_slots, recwarn): + inst = inline_query_result_voice + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, inline_query_result_voice): assert inline_query_result_voice.type == self.type_ assert inline_query_result_voice.id == self.id_ diff --git a/tests/test_inputcontactmessagecontent.py b/tests/test_inputcontactmessagecontent.py index 7d53575fa..b577059a6 100644 --- a/tests/test_inputcontactmessagecontent.py +++ b/tests/test_inputcontactmessagecontent.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import InputContactMessageContent, User @@ -36,6 +35,15 @@ class TestInputContactMessageContent: first_name = 'first name' last_name = 'last name' + def test_slot_behaviour(self, input_contact_message_content, mro_slots, recwarn): + inst = input_contact_message_content + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.first_name = 'should give warning', self.first_name + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_contact_message_content): assert input_contact_message_content.first_name == self.first_name assert input_contact_message_content.phone_number == self.phone_number diff --git a/tests/test_inputfile.py b/tests/test_inputfile.py index 97368ae0d..3b0b4ebd2 100644 --- a/tests/test_inputfile.py +++ b/tests/test_inputfile.py @@ -28,6 +28,15 @@ from telegram import InputFile class TestInputFile: png = os.path.join('tests', 'data', 'game.png') + def test_slot_behaviour(self, recwarn, mro_slots): + inst = InputFile(BytesIO(b'blah'), filename='tg.jpg') + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.filename = 'should give warning', inst.filename + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_subprocess_pipe(self): if sys.platform == 'win32': cmd = ['type', self.png] diff --git a/tests/test_inputinvoicemessagecontent.py b/tests/test_inputinvoicemessagecontent.py index 8af1d21f2..40b0ce0be 100644 --- a/tests/test_inputinvoicemessagecontent.py +++ b/tests/test_inputinvoicemessagecontent.py @@ -74,6 +74,15 @@ class TestInputInvoiceMessageContent: send_email_to_provider = True is_flexible = True + def test_slot_behaviour(self, input_invoice_message_content, recwarn, mro_slots): + inst = input_invoice_message_content + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.title = 'should give warning', self.title + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_invoice_message_content): assert input_invoice_message_content.title == self.title assert input_invoice_message_content.description == self.description diff --git a/tests/test_inputlocationmessagecontent.py b/tests/test_inputlocationmessagecontent.py index 91807bc01..11f679c04 100644 --- a/tests/test_inputlocationmessagecontent.py +++ b/tests/test_inputlocationmessagecontent.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import InputLocationMessageContent, Location @@ -42,6 +41,15 @@ class TestInputLocationMessageContent: heading = 90 proximity_alert_radius = 999 + def test_slot_behaviour(self, input_location_message_content, mro_slots, recwarn): + inst = input_location_message_content + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.heading = 'should give warning', self.heading + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_location_message_content): assert input_location_message_content.longitude == self.longitude assert input_location_message_content.latitude == self.latitude diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index f53335f3e..5f0d10b65 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -127,6 +127,15 @@ class TestInputMediaVideo: supports_streaming = True caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] + def test_slot_behaviour(self, input_media_video, recwarn, mro_slots): + inst = input_media_video + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_media_video): assert input_media_video.type == self.type_ assert input_media_video.media == self.media @@ -185,6 +194,15 @@ class TestInputMediaPhoto: parse_mode = 'Markdown' caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] + def test_slot_behaviour(self, input_media_photo, recwarn, mro_slots): + inst = input_media_photo + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_media_photo): assert input_media_photo.type == self.type_ assert input_media_photo.media == self.media @@ -231,6 +249,15 @@ class TestInputMediaAnimation: height = 30 duration = 1 + def test_slot_behaviour(self, input_media_animation, recwarn, mro_slots): + inst = input_media_animation + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_media_animation): assert input_media_animation.type == self.type_ assert input_media_animation.media == self.media @@ -284,6 +311,15 @@ class TestInputMediaAudio: parse_mode = 'HTML' caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] + def test_slot_behaviour(self, input_media_audio, recwarn, mro_slots): + inst = input_media_audio + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_media_audio): assert input_media_audio.type == self.type_ assert input_media_audio.media == self.media @@ -341,6 +377,15 @@ class TestInputMediaDocument: caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] disable_content_type_detection = True + def test_slot_behaviour(self, input_media_document, recwarn, mro_slots): + inst = input_media_document + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_media_document): assert input_media_document.type == self.type_ assert input_media_document.media == self.media diff --git a/tests/test_inputtextmessagecontent.py b/tests/test_inputtextmessagecontent.py index a352238a2..c996d8fe3 100644 --- a/tests/test_inputtextmessagecontent.py +++ b/tests/test_inputtextmessagecontent.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import InputTextMessageContent, ParseMode, MessageEntity @@ -38,6 +37,15 @@ class TestInputTextMessageContent: entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] disable_web_page_preview = True + def test_slot_behaviour(self, input_text_message_content, mro_slots, recwarn): + inst = input_text_message_content + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.message_text = 'should give warning', self.message_text + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_text_message_content): assert input_text_message_content.parse_mode == self.parse_mode assert input_text_message_content.message_text == self.message_text diff --git a/tests/test_inputvenuemessagecontent.py b/tests/test_inputvenuemessagecontent.py index dee677a5c..1168b91e2 100644 --- a/tests/test_inputvenuemessagecontent.py +++ b/tests/test_inputvenuemessagecontent.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import InputVenueMessageContent, Location @@ -46,6 +45,15 @@ class TestInputVenueMessageContent: google_place_id = 'google place id' google_place_type = 'google place type' + def test_slot_behaviour(self, input_venue_message_content, recwarn, mro_slots): + inst = input_venue_message_content + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.title = 'should give warning', self.title + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, input_venue_message_content): assert input_venue_message_content.longitude == self.longitude assert input_venue_message_content.latitude == self.latitude diff --git a/tests/test_invoice.py b/tests/test_invoice.py index b60c2726b..3011a49e3 100644 --- a/tests/test_invoice.py +++ b/tests/test_invoice.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from flaky import flaky @@ -47,6 +46,14 @@ class TestInvoice: max_tip_amount = 42 suggested_tip_amounts = [13, 42] + def test_slot_behaviour(self, invoice, mro_slots, recwarn): + for attr in invoice.__slots__: + assert getattr(invoice, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not invoice.__dict__, f"got missing slot(s): {invoice.__dict__}" + assert len(mro_slots(invoice)) == len(set(mro_slots(invoice))), "duplicate slot" + invoice.custom, invoice.title = 'should give warning', self.title + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): invoice_json = Invoice.de_json( { diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index eae6e6c8a..d0214a069 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -51,6 +51,14 @@ class TestJobQueue: job_time = 0 received_error = None + def test_slot_behaviour(self, job_queue, recwarn, mro_slots, _dp): + for attr in job_queue.__slots__: + assert getattr(job_queue, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not job_queue.__dict__, f"got missing slot(s): {job_queue.__dict__}" + assert len(mro_slots(job_queue)) == len(set(mro_slots(job_queue))), "duplicate slot" + job_queue.custom, job_queue._dispatcher = 'should give warning', _dp + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.result = 0 diff --git a/tests/test_keyboardbutton.py b/tests/test_keyboardbutton.py index 8ecd2aacd..3c3fd4c04 100644 --- a/tests/test_keyboardbutton.py +++ b/tests/test_keyboardbutton.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import KeyboardButton, InlineKeyboardButton @@ -39,6 +38,15 @@ class TestKeyboardButton: request_contact = True request_poll = KeyboardButtonPollType("quiz") + def test_slot_behaviour(self, keyboard_button, recwarn, mro_slots): + inst = keyboard_button + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.text = 'should give warning', self.text + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, keyboard_button): assert keyboard_button.text == self.text assert keyboard_button.request_location == self.request_location diff --git a/tests/test_keyboardbuttonpolltype.py b/tests/test_keyboardbuttonpolltype.py index 1e7b18d2d..dafe0d9f3 100644 --- a/tests/test_keyboardbuttonpolltype.py +++ b/tests/test_keyboardbuttonpolltype.py @@ -29,6 +29,15 @@ def keyboard_button_poll_type(): class TestKeyboardButtonPollType: type = Poll.QUIZ + def test_slot_behaviour(self, keyboard_button_poll_type, recwarn, mro_slots): + inst = keyboard_button_poll_type + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_to_dict(self, keyboard_button_poll_type): keyboard_button_poll_type_dict = keyboard_button_poll_type.to_dict() assert isinstance(keyboard_button_poll_type_dict, dict) diff --git a/tests/test_labeledprice.py b/tests/test_labeledprice.py index 9e4d3e90e..bfcd72edd 100644 --- a/tests/test_labeledprice.py +++ b/tests/test_labeledprice.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import LabeledPrice, Location @@ -31,6 +30,15 @@ class TestLabeledPrice: label = 'label' amount = 100 + def test_slot_behaviour(self, labeled_price, recwarn, mro_slots): + inst = labeled_price + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.label = 'should give warning', self.label + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, labeled_price): assert labeled_price.label == self.label assert labeled_price.amount == self.amount diff --git a/tests/test_location.py b/tests/test_location.py index ae2115eab..20cd46a11 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from flaky import flaky @@ -44,6 +43,14 @@ class TestLocation: heading = 90 proximity_alert_radius = 50 + def test_slot_behaviour(self, location, recwarn, mro_slots): + for attr in location.__slots__: + assert getattr(location, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not location.__dict__, f"got missing slot(s): {location.__dict__}" + assert len(mro_slots(location)) == len(set(mro_slots(location))), "duplicate slot" + location.custom, location.heading = 'should give warning', self.heading + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'latitude': TestLocation.latitude, diff --git a/tests/test_loginurl.py b/tests/test_loginurl.py index 2880899c8..c638c9234 100644 --- a/tests/test_loginurl.py +++ b/tests/test_loginurl.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import LoginUrl @@ -38,6 +37,14 @@ class TestLoginUrl: bot_username = "botname" request_write_access = True + def test_slot_behaviour(self, login_url, recwarn, mro_slots): + for attr in login_url.__slots__: + assert getattr(login_url, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not login_url.__dict__, f"got missing slot(s): {login_url.__dict__}" + assert len(mro_slots(login_url)) == len(set(mro_slots(login_url))), "duplicate slot" + login_url.custom, login_url.url = 'should give warning', self.url + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_to_dict(self, login_url): login_url_dict = login_url.to_dict() diff --git a/tests/test_message.py b/tests/test_message.py index 64b33475f..3980d050b 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -309,6 +309,14 @@ class TestMessage: caption_entities=[MessageEntity(**e) for e in test_entities_v2], ) + def test_slot_behaviour(self, message, recwarn, mro_slots): + for attr in message.__slots__: + assert getattr(message, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not message.__dict__, f"got missing slot(s): {message.__dict__}" + assert len(mro_slots(message)) == len(set(mro_slots(message))), "duplicate slot" + message.custom, message.message_id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_all_possibilities_de_json_and_to_dict(self, bot, message_params): new = Message.de_json(message_params.to_dict(), bot) @@ -646,7 +654,6 @@ class TestMessage: 'location', 'venue', 'invoice', - 'invoice', 'successful_payment', ): item = getattr(message_params, i, None) diff --git a/tests/test_messageautodeletetimerchanged.py b/tests/test_messageautodeletetimerchanged.py index 22b44ba54..15a62f73e 100644 --- a/tests/test_messageautodeletetimerchanged.py +++ b/tests/test_messageautodeletetimerchanged.py @@ -22,6 +22,15 @@ from telegram import MessageAutoDeleteTimerChanged, VoiceChatEnded class TestMessageAutoDeleteTimerChanged: message_auto_delete_time = 100 + def test_slot_behaviour(self, recwarn, mro_slots): + action = MessageAutoDeleteTimerChanged(self.message_auto_delete_time) + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self): json_dict = {'message_auto_delete_time': self.message_auto_delete_time} madtc = MessageAutoDeleteTimerChanged.de_json(json_dict, None) diff --git a/tests/test_messageentity.py b/tests/test_messageentity.py index 3ce2ace53..2f632c073 100644 --- a/tests/test_messageentity.py +++ b/tests/test_messageentity.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import MessageEntity, User @@ -43,6 +42,15 @@ class TestMessageEntity: length = 2 url = 'url' + def test_slot_behaviour(self, message_entity, recwarn, mro_slots): + inst = message_entity + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = {'type': self.type_, 'offset': self.offset, 'length': self.length} entity = MessageEntity.de_json(json_dict, bot) diff --git a/tests/test_messagehandler.py b/tests/test_messagehandler.py index e7a9e4e87..29d0c3d1c 100644 --- a/tests/test_messagehandler.py +++ b/tests/test_messagehandler.py @@ -71,6 +71,15 @@ class TestMessageHandler: test_flag = False SRE_TYPE = type(re.match("", "")) + def test_slot_behaviour(self, recwarn, mro_slots): + handler = MessageHandler(Filters.all, self.callback_basic) + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_messageid.py b/tests/test_messageid.py index 770cb4519..2573c13d8 100644 --- a/tests/test_messageid.py +++ b/tests/test_messageid.py @@ -15,7 +15,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import MessageId, User @@ -28,6 +27,14 @@ def message_id(): class TestMessageId: m_id = 1234 + def test_slot_behaviour(self, message_id, recwarn, mro_slots): + for attr in message_id.__slots__: + assert getattr(message_id, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not message_id.__dict__, f"got missing slot(s): {message_id.__dict__}" + assert len(mro_slots(message_id)) == len(set(mro_slots(message_id))), "duplicate slot" + message_id.custom, message_id.message_id = 'should give warning', self.m_id + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self): json_dict = {'message_id': self.m_id} message_id = MessageId.de_json(json_dict, None) diff --git a/tests/test_messagequeue.py b/tests/test_messagequeue.py index 548b73233..122207b9f 100644 --- a/tests/test_messagequeue.py +++ b/tests/test_messagequeue.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import os from time import sleep, perf_counter @@ -67,4 +66,4 @@ class TestDelayQueue: passes.append(part) else: fails.append(part) - assert fails == [] + assert not fails diff --git a/tests/test_orderinfo.py b/tests/test_orderinfo.py index 0e5116440..90faeafae 100644 --- a/tests/test_orderinfo.py +++ b/tests/test_orderinfo.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ShippingAddress, OrderInfo @@ -38,6 +37,14 @@ class TestOrderInfo: email = 'email' shipping_address = ShippingAddress('GB', '', 'London', '12 Grimmauld Place', '', 'WC1') + def test_slot_behaviour(self, order_info, mro_slots, recwarn): + for attr in order_info.__slots__: + assert getattr(order_info, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not order_info.__dict__, f"got missing slot(s): {order_info.__dict__}" + assert len(mro_slots(order_info)) == len(set(mro_slots(order_info))), "duplicate slot" + order_info.custom, order_info.name = 'should give warning', self.name + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'name': TestOrderInfo.name, diff --git a/tests/test_parsemode.py b/tests/test_parsemode.py index 2a8902b77..3c7644877 100644 --- a/tests/test_parsemode.py +++ b/tests/test_parsemode.py @@ -29,6 +29,15 @@ class TestParseMode: ) formatted_text_formatted = 'bold italic link name.' + def test_slot_behaviour(self, recwarn, mro_slots): + inst = ParseMode() + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @flaky(3, 1) def test_send_message_with_parse_mode_markdown(self, bot, chat_id): message = bot.send_message( diff --git a/tests/test_passport.py b/tests/test_passport.py index f46a4b234..38687f965 100644 --- a/tests/test_passport.py +++ b/tests/test_passport.py @@ -215,6 +215,15 @@ class TestPassport: driver_license_selfie_credentials_file_hash = 'Cila/qLXSBH7DpZFbb5bRZIRxeFW2uv/ulL0u0JNsYI=' driver_license_selfie_credentials_secret = 'tivdId6RNYNsvXYPppdzrbxOBuBOr9wXRPDcCvnXU7E=' + def test_slot_behaviour(self, passport_data, mro_slots, recwarn): + inst = passport_data + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.data = 'should give warning', passport_data.data + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, passport_data): assert isinstance(passport_data, PassportData) diff --git a/tests/test_passportelementerrordatafield.py b/tests/test_passportelementerrordatafield.py index fd37c780c..2073df2ab 100644 --- a/tests/test_passportelementerrordatafield.py +++ b/tests/test_passportelementerrordatafield.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorDataField, PassportElementErrorSelfie @@ -39,6 +38,15 @@ class TestPassportElementErrorDataField: data_hash = 'data_hash' message = 'Error message' + def test_slot_behaviour(self, passport_element_error_data_field, recwarn, mro_slots): + inst = passport_element_error_data_field + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_data_field): assert passport_element_error_data_field.source == self.source assert passport_element_error_data_field.type == self.type_ diff --git a/tests/test_passportelementerrorfile.py b/tests/test_passportelementerrorfile.py index 2c288aa83..f7dd0c5d8 100644 --- a/tests/test_passportelementerrorfile.py +++ b/tests/test_passportelementerrorfile.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorFile, PassportElementErrorSelfie @@ -37,6 +36,15 @@ class TestPassportElementErrorFile: file_hash = 'file_hash' message = 'Error message' + def test_slot_behaviour(self, passport_element_error_file, recwarn, mro_slots): + inst = passport_element_error_file + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_file): assert passport_element_error_file.source == self.source assert passport_element_error_file.type == self.type_ diff --git a/tests/test_passportelementerrorfiles.py b/tests/test_passportelementerrorfiles.py index f95855141..5dcab832d 100644 --- a/tests/test_passportelementerrorfiles.py +++ b/tests/test_passportelementerrorfiles.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorFiles, PassportElementErrorSelfie @@ -37,6 +36,15 @@ class TestPassportElementErrorFiles: file_hashes = ['hash1', 'hash2'] message = 'Error message' + def test_slot_behaviour(self, passport_element_error_files, mro_slots, recwarn): + inst = passport_element_error_files + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_files): assert passport_element_error_files.source == self.source assert passport_element_error_files.type == self.type_ diff --git a/tests/test_passportelementerrorfrontside.py b/tests/test_passportelementerrorfrontside.py index 00ccb9d1f..fed480e0b 100644 --- a/tests/test_passportelementerrorfrontside.py +++ b/tests/test_passportelementerrorfrontside.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorFrontSide, PassportElementErrorSelfie @@ -37,6 +36,15 @@ class TestPassportElementErrorFrontSide: file_hash = 'file_hash' message = 'Error message' + def test_slot_behaviour(self, passport_element_error_front_side, recwarn, mro_slots): + inst = passport_element_error_front_side + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_front_side): assert passport_element_error_front_side.source == self.source assert passport_element_error_front_side.type == self.type_ diff --git a/tests/test_passportelementerrorreverseside.py b/tests/test_passportelementerrorreverseside.py index b939bd763..a4172e76d 100644 --- a/tests/test_passportelementerrorreverseside.py +++ b/tests/test_passportelementerrorreverseside.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorReverseSide, PassportElementErrorSelfie @@ -37,6 +36,15 @@ class TestPassportElementErrorReverseSide: file_hash = 'file_hash' message = 'Error message' + def test_slot_behaviour(self, passport_element_error_reverse_side, mro_slots, recwarn): + inst = passport_element_error_reverse_side + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_reverse_side): assert passport_element_error_reverse_side.source == self.source assert passport_element_error_reverse_side.type == self.type_ diff --git a/tests/test_passportelementerrorselfie.py b/tests/test_passportelementerrorselfie.py index 46547cafe..ea804012f 100644 --- a/tests/test_passportelementerrorselfie.py +++ b/tests/test_passportelementerrorselfie.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorSelfie, PassportElementErrorDataField @@ -37,6 +36,15 @@ class TestPassportElementErrorSelfie: file_hash = 'file_hash' message = 'Error message' + def test_slot_behaviour(self, passport_element_error_selfie, recwarn, mro_slots): + inst = passport_element_error_selfie + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_selfie): assert passport_element_error_selfie.source == self.source assert passport_element_error_selfie.type == self.type_ diff --git a/tests/test_passportelementerrortranslationfile.py b/tests/test_passportelementerrortranslationfile.py index 999116501..e30d0af76 100644 --- a/tests/test_passportelementerrortranslationfile.py +++ b/tests/test_passportelementerrortranslationfile.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorTranslationFile, PassportElementErrorDataField @@ -37,6 +36,15 @@ class TestPassportElementErrorTranslationFile: file_hash = 'file_hash' message = 'Error message' + def test_slot_behaviour(self, passport_element_error_translation_file, recwarn, mro_slots): + inst = passport_element_error_translation_file + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_translation_file): assert passport_element_error_translation_file.source == self.source assert passport_element_error_translation_file.type == self.type_ diff --git a/tests/test_passportelementerrortranslationfiles.py b/tests/test_passportelementerrortranslationfiles.py index ea65c7ece..5911d59e4 100644 --- a/tests/test_passportelementerrortranslationfiles.py +++ b/tests/test_passportelementerrortranslationfiles.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorTranslationFiles, PassportElementErrorSelfie @@ -37,6 +36,15 @@ class TestPassportElementErrorTranslationFiles: file_hashes = ['hash1', 'hash2'] message = 'Error message' + def test_slot_behaviour(self, passport_element_error_translation_files, mro_slots, recwarn): + inst = passport_element_error_translation_files + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_translation_files): assert passport_element_error_translation_files.source == self.source assert passport_element_error_translation_files.type == self.type_ diff --git a/tests/test_passportelementerrorunspecified.py b/tests/test_passportelementerrorunspecified.py index cebcc7a6e..7a9d67d59 100644 --- a/tests/test_passportelementerrorunspecified.py +++ b/tests/test_passportelementerrorunspecified.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportElementErrorUnspecified, PassportElementErrorDataField @@ -37,6 +36,15 @@ class TestPassportElementErrorUnspecified: element_hash = 'element_hash' message = 'Error message' + def test_slot_behaviour(self, passport_element_error_unspecified, recwarn, mro_slots): + inst = passport_element_error_unspecified + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.type = 'should give warning', self.type_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_element_error_unspecified): assert passport_element_error_unspecified.source == self.source assert passport_element_error_unspecified.type == self.type_ diff --git a/tests/test_passportfile.py b/tests/test_passportfile.py index 0a4af5d6c..ef3b54f6b 100644 --- a/tests/test_passportfile.py +++ b/tests/test_passportfile.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import PassportFile, PassportElementError, Bot, File @@ -40,6 +39,15 @@ class TestPassportFile: file_size = 50 file_date = 1532879128 + def test_slot_behaviour(self, passport_file, mro_slots, recwarn): + inst = passport_file + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.file_id = 'should give warning', self.file_id + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, passport_file): assert passport_file.file_id == self.file_id assert passport_file.file_unique_id == self.file_unique_id diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 82bd22388..10bf010cb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -30,6 +30,7 @@ import os import pickle from collections import defaultdict from time import sleep +from sys import version_info as py_ver import pytest @@ -92,6 +93,8 @@ def base_persistence(): @pytest.fixture(scope="function") def bot_persistence(): class BotPersistence(BasePersistence): + __slots__ = () + def __init__(self): super().__init__() self.bot_data = None @@ -169,6 +172,17 @@ def job_queue(bot): class TestBasePersistence: + def test_slot_behaviour(self, bot_persistence, mro_slots, recwarn): + inst = bot_persistence + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + # assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + # The below test fails if the child class doesn't define __slots__ (not a cause of concern) + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.store_user_data, inst.custom = {}, "custom persistence shouldn't warn" + assert len(recwarn) == 0, recwarn.list + assert '__dict__' not in BasePersistence.__slots__ if py_ver < (3, 7) else True, 'has dict' + def test_creation(self, base_persistence): assert base_persistence.store_chat_data assert base_persistence.store_user_data @@ -802,7 +816,23 @@ def update(bot): return Update(0, message=message) -class TestPickelPersistence: +class TestPicklePersistence: + def test_slot_behaviour(self, mro_slots, recwarn, pickle_persistence): + inst = pickle_persistence + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + # assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.store_user_data = 'should give warning', {} + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + def test_pickle_behaviour_with_slots(self, pickle_persistence): + bot_data = pickle_persistence.get_bot_data() + bot_data['message'] = Message(3, None, Chat(2, type='supergroup')) + pickle_persistence.update_bot_data(bot_data) + retrieved = pickle_persistence.get_bot_data() + assert retrieved == bot_data + def test_no_files_present_multi_file(self, pickle_persistence): assert pickle_persistence.get_user_data() == defaultdict(dict) assert pickle_persistence.get_user_data() == defaultdict(dict) @@ -1412,6 +1442,15 @@ def conversations_json(conversations): class TestDictPersistence: + def test_slot_behaviour(self, mro_slots, recwarn): + inst = DictPersistence() + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + # assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.store_user_data = 'should give warning', {} + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_no_json_given(self): dict_persistence = DictPersistence() assert dict_persistence.get_user_data() == defaultdict(dict) diff --git a/tests/test_photo.py b/tests/test_photo.py index 0f2b3e608..d6096056d 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -66,6 +66,14 @@ class TestPhoto: photo_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.jpg' file_size = 29176 + def test_slot_behaviour(self, photo, recwarn, mro_slots): + for attr in photo.__slots__: + assert getattr(photo, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not photo.__dict__, f"got missing slot(s): {photo.__dict__}" + assert len(mro_slots(photo)) == len(set(mro_slots(photo))), "duplicate slot" + photo.custom, photo.width = 'should give warning', self.width + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, thumb, photo): # Make sure file has been uploaded. assert isinstance(photo, PhotoSize) @@ -235,6 +243,7 @@ class TestPhoto: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_photo(chat_id, file) assert test_flag + monkeypatch.delattr(bot, '_post') @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_poll.py b/tests/test_poll.py index 94114818e..cd93f907c 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -15,7 +15,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from datetime import datetime @@ -34,6 +33,14 @@ class TestPollOption: text = "test option" voter_count = 3 + def test_slot_behaviour(self, poll_option, mro_slots, recwarn): + for attr in poll_option.__slots__: + assert getattr(poll_option, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not poll_option.__dict__, f"got missing slot(s): {poll_option.__dict__}" + assert len(mro_slots(poll_option)) == len(set(mro_slots(poll_option))), "duplicate slot" + poll_option.custom, poll_option.text = 'should give warning', self.text + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self): json_dict = {'text': self.text, 'voter_count': self.voter_count} poll_option = PollOption.de_json(json_dict, None) diff --git a/tests/test_pollanswerhandler.py b/tests/test_pollanswerhandler.py index 15ca04d76..a944c09a3 100644 --- a/tests/test_pollanswerhandler.py +++ b/tests/test_pollanswerhandler.py @@ -74,6 +74,15 @@ def poll_answer(bot): class TestPollAnswerHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + handler = PollAnswerHandler(self.callback_basic) + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + handler.custom, handler.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_pollhandler.py b/tests/test_pollhandler.py index 2047984eb..e4b52148b 100644 --- a/tests/test_pollhandler.py +++ b/tests/test_pollhandler.py @@ -87,6 +87,15 @@ def poll(bot): class TestPollHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + inst = PollHandler(self.callback_basic) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_precheckoutquery.py b/tests/test_precheckoutquery.py index 6f70fb9ff..d9efd8e07 100644 --- a/tests/test_precheckoutquery.py +++ b/tests/test_precheckoutquery.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import Update, User, PreCheckoutQuery, OrderInfo, Bot @@ -46,6 +45,15 @@ class TestPreCheckoutQuery: from_user = User(0, '', False) order_info = OrderInfo() + def test_slot_behaviour(self, pre_checkout_query, recwarn, mro_slots): + inst = pre_checkout_query + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'id': self.id_, diff --git a/tests/test_precheckoutqueryhandler.py b/tests/test_precheckoutqueryhandler.py index fc7f4088a..642a77e36 100644 --- a/tests/test_precheckoutqueryhandler.py +++ b/tests/test_precheckoutqueryhandler.py @@ -79,6 +79,15 @@ def pre_checkout_query(): class TestPreCheckoutQueryHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + inst = PreCheckoutQueryHandler(self.callback_basic) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_promise.py b/tests/test_promise.py index a0768b5c6..ace0a358d 100644 --- a/tests/test_promise.py +++ b/tests/test_promise.py @@ -30,6 +30,15 @@ class TestPromise: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + inst = Promise(self.test_call, [], {}) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.args = 'should give warning', [] + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_proximityalerttriggered.py b/tests/test_proximityalerttriggered.py index 34e4d9ef8..8e09cc00d 100644 --- a/tests/test_proximityalerttriggered.py +++ b/tests/test_proximityalerttriggered.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import BotCommand, User, ProximityAlertTriggered @@ -36,6 +35,15 @@ class TestProximityAlertTriggered: watcher = User(2, 'bar', False) distance = 42 + def test_slot_behaviour(self, proximity_alert_triggered, mro_slots, recwarn): + inst = proximity_alert_triggered + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.traveler = 'should give warning', self.traveler + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'traveler': self.traveler.to_dict(), diff --git a/tests/test_regexhandler.py b/tests/test_regexhandler.py index 5763468ec..03ee1751f 100644 --- a/tests/test_regexhandler.py +++ b/tests/test_regexhandler.py @@ -71,6 +71,15 @@ def message(bot): class TestRegexHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + inst = RegexHandler("", self.callback_basic) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.callback = 'should give warning', self.callback_basic + assert 'custom' in str(recwarn[-1].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index f37870a3a..c5a94ac9b 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from flaky import flaky @@ -39,6 +38,15 @@ class TestReplyKeyboardMarkup: one_time_keyboard = True selective = True + def test_slot_behaviour(self, reply_keyboard_markup, mro_slots, recwarn): + inst = reply_keyboard_markup + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.selective = 'should give warning', self.selective + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @flaky(3, 1) def test_send_message_with_reply_keyboard_markup(self, bot, chat_id, reply_keyboard_markup): message = bot.send_message(chat_id, 'Text', reply_markup=reply_keyboard_markup) diff --git a/tests/test_replykeyboardremove.py b/tests/test_replykeyboardremove.py index ad6bc9425..c948b04e3 100644 --- a/tests/test_replykeyboardremove.py +++ b/tests/test_replykeyboardremove.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from flaky import flaky @@ -32,6 +31,15 @@ class TestReplyKeyboardRemove: remove_keyboard = True selective = True + def test_slot_behaviour(self, reply_keyboard_remove, recwarn, mro_slots): + inst = reply_keyboard_remove + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.selective = 'should give warning', self.selective + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @flaky(3, 1) def test_send_message_with_reply_keyboard_remove(self, bot, chat_id, reply_keyboard_remove): message = bot.send_message(chat_id, 'Text', reply_markup=reply_keyboard_remove) diff --git a/tests/test_request.py b/tests/test_request.py index 454596d22..4442320c8 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -16,13 +16,22 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import TelegramError from telegram.utils.request import Request +def test_slot_behaviour(recwarn, mro_slots): + inst = Request() + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst._connect_timeout = 'should give warning', 10 + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + def test_replaced_unprintable_char(): """ Clients can send arbitrary bytes in callback data. diff --git a/tests/test_shippingaddress.py b/tests/test_shippingaddress.py index 4bc3a2efa..4146cdad0 100644 --- a/tests/test_shippingaddress.py +++ b/tests/test_shippingaddress.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import ShippingAddress @@ -42,6 +41,15 @@ class TestShippingAddress: street_line2 = 'street_line2' post_code = 'WC1' + def test_slot_behaviour(self, shipping_address, recwarn, mro_slots): + inst = shipping_address + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.state = 'should give warning', self.state + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'country_code': self.country_code, diff --git a/tests/test_shippingoption.py b/tests/test_shippingoption.py index 27a101cb0..7f0f0f3fb 100644 --- a/tests/test_shippingoption.py +++ b/tests/test_shippingoption.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import LabeledPrice, ShippingOption, Voice @@ -34,6 +33,15 @@ class TestShippingOption: title = 'title' prices = [LabeledPrice('Fish Container', 100), LabeledPrice('Premium Fish Container', 1000)] + def test_slot_behaviour(self, shipping_option, recwarn, mro_slots): + inst = shipping_option + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self, shipping_option): assert shipping_option.id == self.id_ assert shipping_option.title == self.title diff --git a/tests/test_shippingquery.py b/tests/test_shippingquery.py index 98cf0e01b..58a4783ed 100644 --- a/tests/test_shippingquery.py +++ b/tests/test_shippingquery.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import Update, User, ShippingAddress, ShippingQuery, Bot @@ -40,6 +39,15 @@ class TestShippingQuery: from_user = User(0, '', False) shipping_address = ShippingAddress('GB', '', 'London', '12 Grimmauld Place', '', 'WC1') + def test_slot_behaviour(self, shipping_query, recwarn, mro_slots): + inst = shipping_query + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'id': TestShippingQuery.id_, diff --git a/tests/test_shippingqueryhandler.py b/tests/test_shippingqueryhandler.py index a73743f71..cfa9ecbbd 100644 --- a/tests/test_shippingqueryhandler.py +++ b/tests/test_shippingqueryhandler.py @@ -83,6 +83,15 @@ def shiping_query(): class TestShippingQueryHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + inst = ShippingQueryHandler(self.callback_basic) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_slots.py b/tests/test_slots.py new file mode 100644 index 000000000..eb37db6b5 --- /dev/null +++ b/tests/test_slots.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import importlib +import importlib.util +from glob import iglob + +import inspect + + +excluded = { + 'telegram.error', + '_ConversationTimeoutContext', + 'DispatcherHandlerStop', + 'Days', + 'telegram.deprecate', + 'TelegramDecryptionError', +} # These modules/classes intentionally don't have __dict__. + + +def test_class_has_slots_and_dict(mro_slots): + tg_paths = [p for p in iglob("../telegram/**/*.py", recursive=True) if '/vendor/' not in p] + + for path in tg_paths: + split_path = path.split('/') + mod_name = f"telegram{'.ext.' if split_path[2] == 'ext' else '.'}{split_path[-1][:-3]}" + spec = importlib.util.spec_from_file_location(mod_name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) # Exec module to get classes in it. + + for name, cls in inspect.getmembers(module, inspect.isclass): + if cls.__module__ != module.__name__ or any( # exclude 'imported' modules + x in name for x in {'__class__', '__init__', 'Queue', 'Webhook'} + ): + continue + assert '__slots__' in cls.__dict__, f"class '{name}' in {path} doesn't have __slots__" + if cls.__module__ in excluded or name in excluded: + continue + assert '__dict__' in get_slots(cls), f"class '{name}' in {path} doesn't have __dict__" + + +def get_slots(_class): + return [attr for cls in _class.__mro__ if hasattr(cls, '__slots__') for attr in cls.__slots__] diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 0a1d81d85..bb614b939 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -74,6 +74,14 @@ class TestSticker: sticker_file_id = '5a3128a4d2a04750b5b58397f3b5e812' sticker_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, sticker, mro_slots, recwarn): + for attr in sticker.__slots__: + assert getattr(sticker, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not sticker.__dict__, f"got missing slot(s): {sticker.__dict__}" + assert len(mro_slots(sticker)) == len(set(mro_slots(sticker))), "duplicate slot" + sticker.custom, sticker.emoji = 'should give warning', self.emoji + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, sticker): # Make sure file has been uploaded. assert isinstance(sticker, Sticker) @@ -212,6 +220,7 @@ class TestSticker: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_sticker(chat_id, file) assert test_flag + monkeypatch.delattr(bot, '_post') @flaky(3, 1) @pytest.mark.parametrize( @@ -447,6 +456,7 @@ class TestStickerSet: monkeypatch.setattr(bot, '_post', make_assertion) bot.upload_sticker_file(chat_id, file) assert test_flag + monkeypatch.delattr(bot, '_post') def test_create_new_sticker_set_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up @@ -463,6 +473,7 @@ class TestStickerSet: chat_id, 'name', 'title', 'emoji', png_sticker=file, tgs_sticker=file ) assert test_flag + monkeypatch.delattr(bot, '_post') def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up @@ -477,6 +488,7 @@ class TestStickerSet: monkeypatch.setattr(bot, '_post', make_assertion) bot.add_sticker_to_set(chat_id, 'name', 'emoji', png_sticker=file, tgs_sticker=file) assert test_flag + monkeypatch.delattr(bot, '_post') def test_set_sticker_set_thumb_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up @@ -491,6 +503,7 @@ class TestStickerSet: monkeypatch.setattr(bot, '_post', make_assertion) bot.set_sticker_set_thumb('name', chat_id, thumb=file) assert test_flag + monkeypatch.delattr(bot, '_post') def test_get_file_instance_method(self, monkeypatch, sticker): def make_assertion(*_, **kwargs): diff --git a/tests/test_stringcommandhandler.py b/tests/test_stringcommandhandler.py index f97d2ef69..1fd7ea048 100644 --- a/tests/test_stringcommandhandler.py +++ b/tests/test_stringcommandhandler.py @@ -71,6 +71,15 @@ def false_update(request): class TestStringCommandHandler: test_flag = False + def test_slot_behaviour(self, recwarn, mro_slots): + inst = StringCommandHandler('sleepy', self.callback_basic) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_stringregexhandler.py b/tests/test_stringregexhandler.py index 4d4909fa8..160514c4e 100644 --- a/tests/test_stringregexhandler.py +++ b/tests/test_stringregexhandler.py @@ -71,6 +71,15 @@ def false_update(request): class TestStringRegexHandler: test_flag = False + def test_slot_behaviour(self, mro_slots, recwarn): + inst = StringRegexHandler('pfft', self.callback_basic) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_successfulpayment.py b/tests/test_successfulpayment.py index 63f5de5b3..471f69558 100644 --- a/tests/test_successfulpayment.py +++ b/tests/test_successfulpayment.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from telegram import OrderInfo, SuccessfulPayment @@ -44,6 +43,15 @@ class TestSuccessfulPayment: telegram_payment_charge_id = 'telegram_payment_charge_id' provider_payment_charge_id = 'provider_payment_charge_id' + def test_slot_behaviour(self, successful_payment, recwarn, mro_slots): + inst = successful_payment + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.currency = 'should give warning', self.currency + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'invoice_payload': self.invoice_payload, diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index d75d13ec1..96ae1bd3e 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -77,6 +77,8 @@ class TestTelegramObject: def test_to_dict_private_attribute(self): class TelegramObjectSubclass(TelegramObject): + __slots__ = ('a', '_b') # Added slots so that the attrs are converted to dict + def __init__(self): self.a = 1 self._b = 2 @@ -84,6 +86,15 @@ class TestTelegramObject: subclass_instance = TelegramObjectSubclass() assert subclass_instance.to_dict() == {'a': 1} + def test_slot_behaviour(self, recwarn, mro_slots): + inst = TelegramObject() + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_meaningless_comparison(self, recwarn): expected_warning = "Objects of type TGO can not be meaningfully tested for equivalence." diff --git a/tests/test_typehandler.py b/tests/test_typehandler.py index d5ff9f778..c550dee9f 100644 --- a/tests/test_typehandler.py +++ b/tests/test_typehandler.py @@ -28,6 +28,15 @@ from telegram.ext import TypeHandler, CallbackContext, JobQueue class TestTypeHandler: test_flag = False + def test_slot_behaviour(self, mro_slots, recwarn): + inst = TypeHandler(dict, self.callback_basic) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.callback = 'should give warning', self.callback_basic + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.fixture(autouse=True) def reset(self): self.test_flag = False diff --git a/tests/test_update.py b/tests/test_update.py index b3b7c06dd..2777ff008 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -91,6 +91,14 @@ def update(request): class TestUpdate: update_id = 868573637 + def test_slot_behaviour(self, update, recwarn, mro_slots): + for attr in update.__slots__: + assert getattr(update, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not update.__dict__, f"got missing slot(s): {update.__dict__}" + assert len(mro_slots(update)) == len(set(mro_slots(update))), "duplicate slot" + update.custom, update.update_id = 'should give warning', self.update_id + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + @pytest.mark.parametrize('paramdict', argvalues=params, ids=ids) def test_de_json(self, bot, paramdict): json_dict = {'update_id': TestUpdate.update_id} diff --git a/tests/test_updater.py b/tests/test_updater.py index ab7eccd84..862004d18 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -74,6 +74,25 @@ class TestUpdater: offset = 0 test_flag = False + def test_slot_behaviour(self, updater, mro_slots, recwarn): + for at in updater.__slots__: + at = f"_Updater{at}" if at.startswith('__') and not at.endswith('__') else at + assert getattr(updater, at, 'err') != 'err', f"got extra slot '{at}'" + assert not updater.__dict__, f"got missing slot(s): {updater.__dict__}" + assert len(mro_slots(updater)) == len(set(mro_slots(updater))), "duplicate slot" + updater.custom, updater.running = 'should give warning', updater.running + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + class CustomUpdater(Updater): + pass # Tests that setting custom attributes of Updater subclass doesn't raise warning + + a = CustomUpdater(updater.bot.token) + a.my_custom = 'no error!' + assert len(recwarn) == 1 + + updater.__setattr__('__test', 'mangled success') + assert getattr(updater, '_Updater__test', 'e') == 'mangled success', "mangling failed" + @pytest.fixture(autouse=True) def reset(self): self.message_count = 0 diff --git a/tests/test_user.py b/tests/test_user.py index 807a21d6c..85f75bb8b 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -65,6 +65,14 @@ class TestUser: can_read_all_group_messages = True supports_inline_queries = False + def test_slot_behaviour(self, user, mro_slots, recwarn): + for attr in user.__slots__: + assert getattr(user, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not user.__dict__, f"got missing slot(s): {user.__dict__}" + assert len(mro_slots(user)) == len(set(mro_slots(user))), "duplicate slot" + user.custom, user.id = 'should give warning', self.id_ + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, json_dict, bot): user = User.de_json(json_dict, bot) diff --git a/tests/test_userprofilephotos.py b/tests/test_userprofilephotos.py index b75151e09..84a428da1 100644 --- a/tests/test_userprofilephotos.py +++ b/tests/test_userprofilephotos.py @@ -32,6 +32,15 @@ class TestUserProfilePhotos: ], ] + def test_slot_behaviour(self, recwarn, mro_slots): + inst = UserProfilePhotos(self.total_count, self.photos) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.total_count = 'should give warning', self.total_count + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = {'total_count': 2, 'photos': [[y.to_dict() for y in x] for x in self.photos]} user_profile_photos = UserProfilePhotos.de_json(json_dict, bot) diff --git a/tests/test_venue.py b/tests/test_venue.py index 02a3486e5..185318211 100644 --- a/tests/test_venue.py +++ b/tests/test_venue.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest from flaky import flaky @@ -46,6 +45,14 @@ class TestVenue: google_place_id = 'google place id' google_place_type = 'google place type' + def test_slot_behaviour(self, venue, mro_slots, recwarn): + for attr in venue.__slots__: + assert getattr(venue, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not venue.__dict__, f"got missing slot(s): {venue.__dict__}" + assert len(mro_slots(venue)) == len(set(mro_slots(venue))), "duplicate slot" + venue.custom, venue.title = 'should give warning', self.title + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, bot): json_dict = { 'location': TestVenue.location.to_dict(), diff --git a/tests/test_video.py b/tests/test_video.py index 84534eccf..0eca16798 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -60,6 +60,14 @@ class TestVideo: video_file_id = '5a3128a4d2a04750b5b58397f3b5e812' video_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, video, mro_slots, recwarn): + for attr in video.__slots__: + assert getattr(video, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not video.__dict__, f"got missing slot(s): {video.__dict__}" + assert len(mro_slots(video)) == len(set(mro_slots(video))), "duplicate slot" + video.custom, video.width = 'should give warning', self.width + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, video): # Make sure file has been uploaded. assert isinstance(video, Video) @@ -233,6 +241,7 @@ class TestVideo: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_video(chat_id, file, thumb=file) assert test_flag + monkeypatch.delattr(bot, '_post') @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_videonote.py b/tests/test_videonote.py index f4a97a213..7f8c39773 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -53,6 +53,14 @@ class TestVideoNote: videonote_file_id = '5a3128a4d2a04750b5b58397f3b5e812' videonote_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, video_note, recwarn, mro_slots): + for attr in video_note.__slots__: + assert getattr(video_note, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not video_note.__dict__, f"got missing slot(s): {video_note.__dict__}" + assert len(mro_slots(video_note)) == len(set(mro_slots(video_note))), "duplicate slot" + video_note.custom, video_note.length = 'should give warning', self.length + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, video_note): # Make sure file has been uploaded. assert isinstance(video_note, VideoNote) @@ -171,6 +179,7 @@ class TestVideoNote: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_video_note(chat_id, file, thumb=file) assert test_flag + monkeypatch.delattr(bot, '_post') @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_voice.py b/tests/test_voice.py index a6439dba4..df45da699 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -52,6 +52,14 @@ class TestVoice: voice_file_id = '5a3128a4d2a04750b5b58397f3b5e812' voice_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, voice, recwarn, mro_slots): + for attr in voice.__slots__: + assert getattr(voice, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not voice.__dict__, f"got missing slot(s): {voice.__dict__}" + assert len(mro_slots(voice)) == len(set(mro_slots(voice))), "duplicate slot" + voice.custom, voice.duration = 'should give warning', self.duration + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_creation(self, voice): # Make sure file has been uploaded. assert isinstance(voice, Voice) @@ -195,6 +203,7 @@ class TestVoice: monkeypatch.setattr(bot, '_post', make_assertion) bot.send_voice(chat_id, file) assert test_flag + monkeypatch.delattr(bot, '_post') @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_voicechat.py b/tests/test_voicechat.py index b17b81ffb..8969a2e01 100644 --- a/tests/test_voicechat.py +++ b/tests/test_voicechat.py @@ -40,6 +40,15 @@ def user2(): class TestVoiceChatStarted: + def test_slot_behaviour(self, recwarn, mro_slots): + action = VoiceChatStarted() + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self): voice_chat_started = VoiceChatStarted.de_json({}, None) assert isinstance(voice_chat_started, VoiceChatStarted) @@ -53,6 +62,15 @@ class TestVoiceChatStarted: class TestVoiceChatEnded: duration = 100 + def test_slot_behaviour(self, recwarn, mro_slots): + action = VoiceChatEnded(8) + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self): json_dict = {'duration': self.duration} voice_chat_ended = VoiceChatEnded.de_json(json_dict, None) @@ -83,6 +101,15 @@ class TestVoiceChatEnded: class TestVoiceChatParticipantsInvited: + def test_slot_behaviour(self, recwarn, mro_slots): + action = VoiceChatParticipantsInvited([user1]) + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_de_json(self, user1, user2, bot): json_data = {"users": [user1.to_dict(), user2.to_dict()]} voice_chat_participants = VoiceChatParticipantsInvited.de_json(json_data, bot) @@ -125,6 +152,15 @@ class TestVoiceChatParticipantsInvited: class TestVoiceChatScheduled: start_date = dtm.datetime.utcnow() + def test_slot_behaviour(self, recwarn, mro_slots): + inst = VoiceChatScheduled(self.start_date) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.start_date = 'should give warning', self.start_date + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_expected_values(self): assert pytest.approx(VoiceChatScheduled(start_date=self.start_date) == self.start_date) diff --git a/tests/test_webhookinfo.py b/tests/test_webhookinfo.py index 81eefa21b..9b07932f5 100644 --- a/tests/test_webhookinfo.py +++ b/tests/test_webhookinfo.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - import pytest import time @@ -45,6 +44,14 @@ class TestWebhookInfo: max_connections = 42 allowed_updates = ['type1', 'type2'] + def test_slot_behaviour(self, webhook_info, mro_slots, recwarn): + for attr in webhook_info.__slots__: + assert getattr(webhook_info, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not webhook_info.__dict__, f"got missing slot(s): {webhook_info.__dict__}" + assert len(mro_slots(webhook_info)) == len(set(mro_slots(webhook_info))), "duplicate slot" + webhook_info.custom, webhook_info.url = 'should give warning', self.url + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + def test_to_dict(self, webhook_info): webhook_info_dict = webhook_info.to_dict()