Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Freeze empty classes #3453

Merged
merged 2 commits into from Jan 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions telegram/_forumtopic.py
Expand Up @@ -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):
"""
Expand All @@ -132,3 +137,8 @@ class ForumTopicReopened(TelegramObject):
"""

__slots__ = ()

def __init__(self, *, api_kwargs: JSONDict = None) -> None:
super().__init__(api_kwargs=api_kwargs)

self._freeze()
6 changes: 6 additions & 0 deletions telegram/_games/callbackgame.py
Expand Up @@ -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()
18 changes: 8 additions & 10 deletions telegram/_inline/inputcontactmessagecontent.py
Expand Up @@ -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,)
65 changes: 32 additions & 33 deletions telegram/_inline/inputinvoicemessagecontent.py
Expand Up @@ -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(
Expand Down
29 changes: 14 additions & 15 deletions telegram/_inline/inputlocationmessagecontent.py
Expand Up @@ -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`
Expand Down
6 changes: 6 additions & 0 deletions telegram/_inline/inputmessagecontent.py
Expand Up @@ -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):
Expand All @@ -32,3 +33,8 @@ class InputMessageContent(TelegramObject):
"""

__slots__ = ()

def __init__(self, *, api_kwargs: JSONDict = None) -> None:
super().__init__(api_kwargs=api_kwargs)

self._freeze()
19 changes: 9 additions & 10 deletions telegram/_inline/inputtextmessagecontent.py
Expand Up @@ -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,)
34 changes: 16 additions & 18 deletions telegram/_inline/inputvenuemessagecontent.py
Expand Up @@ -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,
)
4 changes: 4 additions & 0 deletions telegram/_telegramobject.py
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions telegram/_videochat.py
Expand Up @@ -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):
harshil21 marked this conversation as resolved.
Show resolved Hide resolved
"""
Expand Down
1 change: 1 addition & 0 deletions tests/test_bot.py
Expand Up @@ -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

Expand Down
14 changes: 11 additions & 3 deletions tests/test_telegramobject.py
Expand Up @@ -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
Expand All @@ -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
Expand Down