From 7b61a30fb10912c2849c8a25294207cb1a3233c7 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 1 Jan 2023 13:04:37 +0100 Subject: [PATCH] Freeze Classes Without Arguments (#3453) --- telegram/_forumtopic.py | 10 +++ telegram/_games/callbackgame.py | 6 ++ .../_inline/inputcontactmessagecontent.py | 18 +++--- .../_inline/inputinvoicemessagecontent.py | 63 +++++++++---------- .../_inline/inputlocationmessagecontent.py | 25 ++++---- telegram/_inline/inputmessagecontent.py | 6 ++ telegram/_inline/inputtextmessagecontent.py | 17 +++-- telegram/_inline/inputvenuemessagecontent.py | 34 +++++----- telegram/_telegramobject.py | 4 ++ telegram/_videochat.py | 5 ++ tests/test_bot.py | 1 + tests/test_telegramobject.py | 14 ++++- 12 files changed, 118 insertions(+), 85 deletions(-) diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py index 38684118d..cc77febe1 100644 --- a/telegram/_forumtopic.py +++ b/telegram/_forumtopic.py @@ -122,6 +122,11 @@ class ForumTopicClosed(TelegramObject): __slots__ = () + def __init__(self, *, api_kwargs: JSONDict = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + class ForumTopicReopened(TelegramObject): """ @@ -132,3 +137,8 @@ class ForumTopicReopened(TelegramObject): """ __slots__ = () + + def __init__(self, *, api_kwargs: JSONDict = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() diff --git a/telegram/_games/callbackgame.py b/telegram/_games/callbackgame.py index dc97acaf2..a8441a523 100644 --- a/telegram/_games/callbackgame.py +++ b/telegram/_games/callbackgame.py @@ -19,9 +19,15 @@ """This module contains an object that represents a Telegram CallbackGame.""" from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict class CallbackGame(TelegramObject): """A placeholder, currently holds no information. Use BotFather to set up your game.""" __slots__ = () + + def __init__(self, *, api_kwargs: JSONDict = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() diff --git a/telegram/_inline/inputcontactmessagecontent.py b/telegram/_inline/inputcontactmessagecontent.py index 132d8f85b..8d19ce216 100644 --- a/telegram/_inline/inputcontactmessagecontent.py +++ b/telegram/_inline/inputcontactmessagecontent.py @@ -56,14 +56,12 @@ class InputContactMessageContent(InputMessageContent): api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) + with self._unfrozen(): + # Required + self.phone_number = phone_number + self.first_name = first_name + # Optionals + self.last_name = last_name + self.vcard = vcard - # Required - self.phone_number = phone_number - self.first_name = first_name - # Optionals - self.last_name = last_name - self.vcard = vcard - - self._id_attrs = (self.phone_number,) - - self._freeze() + self._id_attrs = (self.phone_number,) diff --git a/telegram/_inline/inputinvoicemessagecontent.py b/telegram/_inline/inputinvoicemessagecontent.py index cfdf0db98..93781fec5 100644 --- a/telegram/_inline/inputinvoicemessagecontent.py +++ b/telegram/_inline/inputinvoicemessagecontent.py @@ -200,39 +200,38 @@ class InputInvoiceMessageContent(InputMessageContent): api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) - # Required - self.title = title - self.description = description - self.payload = payload - self.provider_token = provider_token - self.currency = currency - self.prices = parse_sequence_arg(prices) - # Optionals - self.max_tip_amount = max_tip_amount - self.suggested_tip_amounts = parse_sequence_arg(suggested_tip_amounts) - self.provider_data = provider_data - self.photo_url = photo_url - self.photo_size = photo_size - self.photo_width = photo_width - self.photo_height = photo_height - self.need_name = need_name - self.need_phone_number = need_phone_number - self.need_email = need_email - self.need_shipping_address = need_shipping_address - self.send_phone_number_to_provider = send_phone_number_to_provider - self.send_email_to_provider = send_email_to_provider - self.is_flexible = is_flexible + with self._unfrozen(): + # Required + self.title = title + self.description = description + self.payload = payload + self.provider_token = provider_token + self.currency = currency + self.prices = parse_sequence_arg(prices) + # Optionals + self.max_tip_amount = max_tip_amount + self.suggested_tip_amounts = parse_sequence_arg(suggested_tip_amounts) + self.provider_data = provider_data + self.photo_url = photo_url + self.photo_size = photo_size + self.photo_width = photo_width + self.photo_height = photo_height + self.need_name = need_name + self.need_phone_number = need_phone_number + self.need_email = need_email + self.need_shipping_address = need_shipping_address + self.send_phone_number_to_provider = send_phone_number_to_provider + self.send_email_to_provider = send_email_to_provider + self.is_flexible = is_flexible - self._id_attrs = ( - self.title, - self.description, - self.payload, - self.provider_token, - self.currency, - self.prices, - ) - - self._freeze() + self._id_attrs = ( + self.title, + self.description, + self.payload, + self.provider_token, + self.currency, + self.prices, + ) @classmethod def de_json( diff --git a/telegram/_inline/inputlocationmessagecontent.py b/telegram/_inline/inputlocationmessagecontent.py index f3806dfae..a0a00285c 100644 --- a/telegram/_inline/inputlocationmessagecontent.py +++ b/telegram/_inline/inputlocationmessagecontent.py @@ -83,21 +83,20 @@ class InputLocationMessageContent(InputMessageContent): api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) - # Required - self.latitude = latitude - self.longitude = longitude + with self._unfrozen(): + # Required + self.latitude = latitude + self.longitude = longitude - # Optionals - self.live_period = live_period - self.horizontal_accuracy = horizontal_accuracy - self.heading = heading - self.proximity_alert_radius = ( - int(proximity_alert_radius) if proximity_alert_radius else None - ) + # Optionals + self.live_period = live_period + self.horizontal_accuracy = horizontal_accuracy + self.heading = heading + self.proximity_alert_radius = ( + int(proximity_alert_radius) if proximity_alert_radius else None + ) - self._id_attrs = (self.latitude, self.longitude) - - self._freeze() + self._id_attrs = (self.latitude, self.longitude) HORIZONTAL_ACCURACY: ClassVar[int] = constants.LocationLimit.HORIZONTAL_ACCURACY """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` diff --git a/telegram/_inline/inputmessagecontent.py b/telegram/_inline/inputmessagecontent.py index 38e918e5b..846de3756 100644 --- a/telegram/_inline/inputmessagecontent.py +++ b/telegram/_inline/inputmessagecontent.py @@ -19,6 +19,7 @@ """This module contains the classes that represent Telegram InputMessageContent.""" from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict class InputMessageContent(TelegramObject): @@ -32,3 +33,8 @@ class InputMessageContent(TelegramObject): """ __slots__ = () + + def __init__(self, *, api_kwargs: JSONDict = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() diff --git a/telegram/_inline/inputtextmessagecontent.py b/telegram/_inline/inputtextmessagecontent.py index e2a782559..6d23b26a8 100644 --- a/telegram/_inline/inputtextmessagecontent.py +++ b/telegram/_inline/inputtextmessagecontent.py @@ -78,13 +78,12 @@ class InputTextMessageContent(InputMessageContent): api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) - # Required - self.message_text = message_text - # Optionals - self.parse_mode = parse_mode - self.entities = parse_sequence_arg(entities) - self.disable_web_page_preview = disable_web_page_preview + with self._unfrozen(): + # Required + self.message_text = message_text + # Optionals + self.parse_mode = parse_mode + self.entities = parse_sequence_arg(entities) + self.disable_web_page_preview = disable_web_page_preview - self._id_attrs = (self.message_text,) - - self._freeze() + self._id_attrs = (self.message_text,) diff --git a/telegram/_inline/inputvenuemessagecontent.py b/telegram/_inline/inputvenuemessagecontent.py index b44679bc5..15ef7293f 100644 --- a/telegram/_inline/inputvenuemessagecontent.py +++ b/telegram/_inline/inputvenuemessagecontent.py @@ -84,22 +84,20 @@ class InputVenueMessageContent(InputMessageContent): api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) + with self._unfrozen(): + # Required + self.latitude = latitude + self.longitude = longitude + self.title = title + self.address = address + # Optionals + self.foursquare_id = foursquare_id + self.foursquare_type = foursquare_type + self.google_place_id = google_place_id + self.google_place_type = google_place_type - # Required - self.latitude = latitude - self.longitude = longitude - self.title = title - self.address = address - # Optionals - self.foursquare_id = foursquare_id - self.foursquare_type = foursquare_type - self.google_place_id = google_place_id - self.google_place_type = google_place_type - - self._id_attrs = ( - self.latitude, - self.longitude, - self.title, - ) - - self._freeze() + self._id_attrs = ( + self.latitude, + self.longitude, + self.title, + ) diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index a15e1d79c..ecd340ad1 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -92,6 +92,10 @@ class TelegramObject: __INIT_PARAMS_CHECK: Optional[Type["TelegramObject"]] = None def __init__(self, *, api_kwargs: JSONDict = None) -> None: + # Setting _frozen to `False` here means that classes without arguments still need to + # implement __init__. However, with `True` would mean increased usage of + # `with self._unfrozen()` in the `__init__` of subclasses and we have fewer empty + # classes than classes with arguments. self._frozen: bool = False self._id_attrs: Tuple[object, ...] = () self._bot: Optional["Bot"] = None diff --git a/telegram/_videochat.py b/telegram/_videochat.py index a32a74785..49ebe7718 100644 --- a/telegram/_videochat.py +++ b/telegram/_videochat.py @@ -42,6 +42,11 @@ class VideoChatStarted(TelegramObject): __slots__ = () + def __init__(self, *, api_kwargs: JSONDict = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + class VideoChatEnded(TelegramObject): """ diff --git a/tests/test_bot.py b/tests/test_bot.py index 916461bcc..501c49603 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -185,6 +185,7 @@ class InputMessageContentDWPP(InputMessageContent): api_kwargs=None, ): super().__init__(api_kwargs=api_kwargs) + self._unfreeze() self.message_text = message_text self.disable_web_page_preview = disable_web_page_preview diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 132f70770..a06e96e49 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -428,7 +428,8 @@ class TestTelegramObject: @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) def test_subclasses_are_frozen(self, cls): - if cls.__name__.startswith("_"): + if cls is TelegramObject or cls.__name__.startswith("_"): + # Protected classes don't need to be frozen and neither does the base class return # instantiating each subclass would be tedious as some attributes require special init @@ -437,10 +438,17 @@ class TestTelegramObject: source_file = inspect.getsourcefile(cls.__init__) parents = Path(source_file).parents is_test_file = Path(__file__).parent.resolve() in parents - if is_test_file or source_file.endswith("telegramobject.py"): - # classes without their own `__init__` can be ignored + + if is_test_file: + # If the class is defined in a test file, we don't want to test it. return + if source_file.endswith("telegramobject.py"): + pytest.fail( + f"{cls.__name__} does not have its own `__init__` " + "and can therefore not be frozen correctly" + ) + source_lines, first_line = inspect.getsourcelines(cls.__init__) # We use regex matching since a simple "if self._freeze() in source_lines[-1]" would also