From a3923291d05e7d21cf9b6279f71c301174638063 Mon Sep 17 00:00:00 2001 From: starry69 Date: Fri, 15 Jan 2021 22:15:05 +0530 Subject: [PATCH 1/5] Make telegram.Bot comparable Signed-off-by: starry69 --- .gitignore | 3 +++ telegram/bot.py | 46 +++++++++++++++++----------------------------- tests/test_bot.py | 6 ++++++ 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index a2e9366ddaf..85a61e2b5c0 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ telegram.jpg # Exclude .exrc file for Vim .exrc + +# virtual env +venv* diff --git a/telegram/bot.py b/telegram/bot.py index 04aaa49ac69..84c71ef9521 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -110,22 +110,6 @@ RT = TypeVar('RT') -def info(func: Callable[..., RT]) -> Callable[..., RT]: - # pylint: disable=W0212 - @functools.wraps(func) - def decorator(self: 'Bot', *args: Any, **kwargs: Any) -> RT: - if not self.bot: - self.get_me() - - if self._commands is None: - self.get_my_commands() - - result = func(self, *args, **kwargs) - return result - - return decorator - - def log( func: Callable[..., RT], *args: Any, **kwargs: Any # pylint: disable=W0613 ) -> Callable[..., RT]: @@ -217,7 +201,7 @@ def __init__( self.base_url = str(base_url) + str(self.token) self.base_file_url = str(base_file_url) + str(self.token) - self.bot: Optional[User] = None + self._bot: Optional[User] = None self._commands: Optional[List[BotCommand]] = None self._request = request or Request() self.logger = logging.getLogger(__name__) @@ -303,67 +287,68 @@ def _validate_token(token: str) -> str: return token @property # type: ignore - @info + def bot(self) -> User: + """:class:`telegram.User`: Instance for the bot.""" + + if self._bot is None: + self.get_me() + return self._bot + + @property # type: ignore def id(self) -> int: """:obj:`int`: Unique identifier for this bot.""" return self.bot.id # type: ignore @property # type: ignore - @info def first_name(self) -> str: """:obj:`str`: Bot's first name.""" return self.bot.first_name # type: ignore @property # type: ignore - @info def last_name(self) -> str: """:obj:`str`: Optional. Bot's last name.""" return self.bot.last_name # type: ignore @property # type: ignore - @info def username(self) -> str: """:obj:`str`: Bot's username.""" return self.bot.username # type: ignore @property # type: ignore - @info def link(self) -> str: """:obj:`str`: Convenience property. Returns the t.me link of the bot.""" return f"https://t.me/{self.username}" @property # type: ignore - @info def can_join_groups(self) -> bool: """:obj:`bool`: Bot's can_join_groups attribute.""" return self.bot.can_join_groups # type: ignore @property # type: ignore - @info def can_read_all_group_messages(self) -> bool: """:obj:`bool`: Bot's can_read_all_group_messages attribute.""" return self.bot.can_read_all_group_messages # type: ignore @property # type: ignore - @info def supports_inline_queries(self) -> bool: """:obj:`bool`: Bot's supports_inline_queries attribute.""" return self.bot.supports_inline_queries # type: ignore @property # type: ignore - @info def commands(self) -> List[BotCommand]: """List[:class:`BotCommand`]: Bot's commands.""" - return self._commands or [] + if self._commands is None: + self.get_my_commands() + return self._commands @property def name(self) -> str: @@ -392,9 +377,9 @@ def get_me(self, timeout: int = None, api_kwargs: JSONDict = None) -> User: """ result = self._post('getMe', timeout=timeout, api_kwargs=api_kwargs) - self.bot = User.de_json(result, self) # type: ignore + self._bot = User.de_json(result, self) # type: ignore - return self.bot # type: ignore[return-value] + return self._bot # type: ignore[return-value] @log def send_message( @@ -4814,6 +4799,9 @@ def to_dict(self) -> JSONDict: return data + def __eq__(self, other: object) -> bool: + return self.bot == other + # camelCase aliases getMe = get_me """Alias for :attr:`get_me`""" diff --git a/tests/test_bot.py b/tests/test_bot.py index 8777495b3f8..69aff3fb058 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -145,6 +145,12 @@ def test_get_me_and_properties(self, bot): assert f'https://t.me/{get_me_bot.username}' == bot.link assert commands == bot.commands + def test_comparison(self, bot): + bot1 = bot.get_me() + + assert isinstance(bot1, User) + assert bot1 == bot + @flaky(3, 1) @pytest.mark.timeout(10) def test_to_dict(self, bot): From 8e62767404264eb5b79707ba2c786c4c93f82544 Mon Sep 17 00:00:00 2001 From: starry69 Date: Fri, 15 Jan 2021 23:36:30 +0530 Subject: [PATCH 2/5] Address review Signed-off-by: starry69 --- telegram/bot.py | 30 +++++++++++++++--------------- tests/test_bot.py | 7 ++++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 84c71ef9521..41fa6c1a2e4 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -286,68 +286,68 @@ def _validate_token(token: str) -> str: return token - @property # type: ignore + @property def bot(self) -> User: - """:class:`telegram.User`: Instance for the bot.""" + """:class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`.""" if self._bot is None: - self.get_me() + self._bot = self.get_me() return self._bot - @property # type: ignore + @property def id(self) -> int: """:obj:`int`: Unique identifier for this bot.""" - return self.bot.id # type: ignore + return self.bot.id - @property # type: ignore + @property def first_name(self) -> str: """:obj:`str`: Bot's first name.""" - return self.bot.first_name # type: ignore + return self.bot.first_name - @property # type: ignore + @property def last_name(self) -> str: """:obj:`str`: Optional. Bot's last name.""" return self.bot.last_name # type: ignore - @property # type: ignore + @property def username(self) -> str: """:obj:`str`: Bot's username.""" return self.bot.username # type: ignore - @property # type: ignore + @property def link(self) -> str: """:obj:`str`: Convenience property. Returns the t.me link of the bot.""" return f"https://t.me/{self.username}" - @property # type: ignore + @property def can_join_groups(self) -> bool: """:obj:`bool`: Bot's can_join_groups attribute.""" return self.bot.can_join_groups # type: ignore - @property # type: ignore + @property def can_read_all_group_messages(self) -> bool: """:obj:`bool`: Bot's can_read_all_group_messages attribute.""" return self.bot.can_read_all_group_messages # type: ignore - @property # type: ignore + @property def supports_inline_queries(self) -> bool: """:obj:`bool`: Bot's supports_inline_queries attribute.""" return self.bot.supports_inline_queries # type: ignore - @property # type: ignore + @property def commands(self) -> List[BotCommand]: """List[:class:`BotCommand`]: Bot's commands.""" if self._commands is None: - self.get_my_commands() + self._commands = self.get_my_commands() return self._commands @property diff --git a/tests/test_bot.py b/tests/test_bot.py index 69aff3fb058..114b64b7753 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -146,10 +146,11 @@ def test_get_me_and_properties(self, bot): assert commands == bot.commands def test_comparison(self, bot): - bot1 = bot.get_me() + get_me_bot = bot.get_me() - assert isinstance(bot1, User) - assert bot1 == bot + assert isinstance(get_me_bot, User) + assert get_me_bot == bot + assert get_me_bot in (bot,) @flaky(3, 1) @pytest.mark.timeout(10) From 987318ac442f3a77fa09070f0733d0bb5ea6610a Mon Sep 17 00:00:00 2001 From: starry69 Date: Sat, 16 Jan 2021 22:11:37 +0530 Subject: [PATCH 3/5] Enhance tests & add docstring about comparison Signed-off-by: starry69 --- telegram/bot.py | 7 +++++++ tests/test_bot.py | 21 ++++++++++++++++----- tests/test_updater.py | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 41fa6c1a2e4..48e5df9d556 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -128,6 +128,10 @@ def decorator(self: 'Bot', *args: Any, **kwargs: Any) -> RT: # pylint: disable= class Bot(TelegramObject): """This object represents a Telegram Bot. + .. versionadded:: 13.2 + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`bot` is equal. + Note: Most bot methods have the argument ``api_kwargs`` which allows to pass arbitrary keywords to the Telegram API. This can be used to access new features of the API before they were @@ -4802,6 +4806,9 @@ def to_dict(self) -> JSONDict: def __eq__(self, other: object) -> bool: return self.bot == other + def __hash__(self) -> int: + return hash(self.bot) + # camelCase aliases getMe = get_me """Alias for :attr:`get_me`""" diff --git a/tests/test_bot.py b/tests/test_bot.py index 114b64b7753..7e935ea5c0c 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -48,6 +48,8 @@ from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter from telegram.utils.helpers import from_timestamp, escape_markdown, to_timestamp from tests.conftest import expect_bad_request +from tests.bots import FALLBACKS + BASE_TIME = time.time() HIGHSCORE_DELTA = 1450000000 @@ -145,12 +147,21 @@ def test_get_me_and_properties(self, bot): assert f'https://t.me/{get_me_bot.username}' == bot.link assert commands == bot.commands - def test_comparison(self, bot): - get_me_bot = bot.get_me() + def test_equality(self): + a = Bot(FALLBACKS[0]["token"]) + b = Bot(FALLBACKS[0]["token"]) + c = Bot(FALLBACKS[1]["token"]) + d = Update(123456789) - assert isinstance(get_me_bot, User) - assert get_me_bot == bot - assert get_me_bot in (bot,) + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) @flaky(3, 1) @pytest.mark.timeout(10) diff --git a/tests/test_updater.py b/tests/test_updater.py index 98ae0685fe0..e246496c38c 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -208,7 +208,7 @@ def test_start_webhook_no_warning_or_error_logs(self, caplog, updater, monkeypat monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) # prevent api calls from @info decorator when updater.bot.id is used in thread names - monkeypatch.setattr(updater.bot, 'bot', User(id=123, first_name='bot', is_bot=True)) + monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True)) monkeypatch.setattr(updater.bot, '_commands', []) ip = '127.0.0.1' From dd8d3002902e0edca634066b987510bce57695d1 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 17 Jan 2021 09:01:35 +0100 Subject: [PATCH 4/5] Minor doc fix --- telegram/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 48e5df9d556..828e476b9ff 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -129,8 +129,8 @@ class Bot(TelegramObject): """This object represents a Telegram Bot. .. versionadded:: 13.2 - Objects of this class are comparable in terms of equality. Two objects of this class are - considered equal, if their :attr:`bot` is equal. + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`bot` is equal. Note: Most bot methods have the argument ``api_kwargs`` which allows to pass arbitrary keywords From b1b99c049f919fae7cd0e9c72f0265026cbb176e Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 17 Jan 2021 09:13:19 +0100 Subject: [PATCH 5/5] Extend tests --- tests/test_bot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 7e935ea5c0c..20c72b3bb73 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -146,6 +146,8 @@ def test_get_me_and_properties(self, bot): assert get_me_bot.supports_inline_queries == bot.supports_inline_queries assert f'https://t.me/{get_me_bot.username}' == bot.link assert commands == bot.commands + bot._commands = None + assert commands == bot.commands def test_equality(self): a = Bot(FALLBACKS[0]["token"])