diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py index 38684118dac..cc77febe15b 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 dc97acaf29b..a8441a5237b 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 132d8f85b11..8d19ce2161b 100644 --- a/telegram/_inline/inputcontactmessagecontent.py +++ b/telegram/_inline/inputcontactmessagecontent.py @@ -56,14 +56,12 @@ def __init__( 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 cfdf0db980a..93781fec584 100644 --- a/telegram/_inline/inputinvoicemessagecontent.py +++ b/telegram/_inline/inputinvoicemessagecontent.py @@ -200,39 +200,38 @@ def __init__( 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 - - self._id_attrs = ( - self.title, - self.description, - self.payload, - self.provider_token, - self.currency, - self.prices, - ) - - self._freeze() + 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, + ) @classmethod def de_json( diff --git a/telegram/_inline/inputlocationmessagecontent.py b/telegram/_inline/inputlocationmessagecontent.py index f3806dfae6b..a0a00285cb9 100644 --- a/telegram/_inline/inputlocationmessagecontent.py +++ b/telegram/_inline/inputlocationmessagecontent.py @@ -83,21 +83,20 @@ def __init__( api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) - # 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 - ) - - self._id_attrs = (self.latitude, self.longitude) - - self._freeze() + 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 + ) + + 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 38e918e5b86..846de3756fe 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 e2a7825595a..6d23b26a8d4 100644 --- a/telegram/_inline/inputtextmessagecontent.py +++ b/telegram/_inline/inputtextmessagecontent.py @@ -78,13 +78,12 @@ def __init__( 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 - - self._id_attrs = (self.message_text,) - - self._freeze() + 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,) diff --git a/telegram/_inline/inputvenuemessagecontent.py b/telegram/_inline/inputvenuemessagecontent.py index b44679bc589..15ef7293f4e 100644 --- a/telegram/_inline/inputvenuemessagecontent.py +++ b/telegram/_inline/inputvenuemessagecontent.py @@ -84,22 +84,20 @@ def __init__( 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 a15e1d79c86..ecd340ad19b 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 a32a74785c3..49ebe771884 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 916461bccf0..501c4960332 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -185,6 +185,7 @@ def __init__( 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 132f70770f1..a06e96e4935 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -428,7 +428,8 @@ def __init__(self, api_kwargs=None): @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 @@ def test_subclasses_are_frozen(self, cls): 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