From 2fb8ef6c74488af54dd90ec81364ab969512ecf1 Mon Sep 17 00:00:00 2001 From: david-shiko Date: Fri, 7 Mar 2025 04:31:36 +0300 Subject: [PATCH 1/4] Add `name` and `full_name` properties to the `SharedUser` class just as in the `telegram.User` class. --- telegram/_shared.py | 22 +++++++++++++++++++++- tests/test_shared.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/telegram/_shared.py b/telegram/_shared.py index 9c0d3684ec2..77055624631 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains two objects used for request chats/users service messages.""" from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject @@ -244,6 +244,26 @@ def __init__( self._freeze() + @property + def name(self) -> Union[str, None]: + """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` + prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`. + """ + if self.username: + return f"@{self.username}" + return self.full_name + + @property + def full_name(self) -> Union[str, None]: + """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if + available) :attr:`last_name`, otherwise None. + """ + if self.first_name and self.last_name: + return f"{self.first_name} {self.last_name}" + if self.first_name or self.last_name: + return f"{self.first_name or self.last_name}" + return None + @classmethod def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SharedUser": """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/tests/test_shared.py b/tests/test_shared.py index 239e8600092..61478b7a8aa 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -17,6 +17,8 @@ # 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 unittest.mock import patch + import pytest from telegram import ChatShared, PhotoSize, SharedUser, UsersShared @@ -228,3 +230,32 @@ def test_equality(self, chat_shared): assert a != d assert hash(a) != hash(d) + + def test_name(self, shared_user, ): + with shared_user._unfrozen(): + assert shared_user.name == "@user" + with patch.object(shared_user, 'username', None): + assert shared_user.name == "first last" + with patch.multiple( + shared_user, + last_name=None, + first_name=None, + ): + assert shared_user.full_name is None + + def test_full_name(self, shared_user): + with shared_user._unfrozen(): + # Test `and` (both exists) + assert shared_user.full_name == "first last" + # Test `or` (one of them exists) + with patch.object(shared_user, 'first_name', None): + assert shared_user.full_name == "last" + with patch.object(shared_user, 'last_name', None): + assert shared_user.full_name == "first" + # Test None (None of them exists) + with patch.multiple( + shared_user, + last_name=None, + first_name=None, + ): + assert shared_user.full_name is None From 91a8747ee2a6ed8c5ca5facd1f5949a30599287a Mon Sep 17 00:00:00 2001 From: david-shiko Date: Mon, 10 Mar 2025 03:37:18 +0300 Subject: [PATCH 2/4] Initial commit; Add functions to get name, full name and link; Add tests for it; Replace `user` and `shared_user` correspond properties implementations on a new functinos. --- telegram/_shared.py | 18 +++--- telegram/_user.py | 13 ++--- telegram/_utils/usernames.py | 104 +++++++++++++++++++++++++++++++++ tests/_utils/test_usernames.py | 71 ++++++++++++++++++++++ tests/test_shared.py | 28 --------- 5 files changed, 189 insertions(+), 45 deletions(-) create mode 100644 telegram/_utils/usernames.py create mode 100644 tests/_utils/test_usernames.py diff --git a/telegram/_shared.py b/telegram/_shared.py index 77055624631..f22ccd1a4c7 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -24,6 +24,7 @@ from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg from telegram._utils.types import JSONDict +from telegram._utils.usernames import get_name, get_full_name, get_link if TYPE_CHECKING: from telegram._bot import Bot @@ -249,20 +250,21 @@ def name(self) -> Union[str, None]: """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`. """ - if self.username: - return f"@{self.username}" - return self.full_name + return get_name(user=self, ) @property def full_name(self) -> Union[str, None]: """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if available) :attr:`last_name`, otherwise None. """ - if self.first_name and self.last_name: - return f"{self.first_name} {self.last_name}" - if self.first_name or self.last_name: - return f"{self.first_name or self.last_name}" - return None + return get_full_name(user=self, ) + + @property + def link(self) -> Union[str, None]: + """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if + available) :attr:`last_name`, otherwise None. + """ + return get_link(user=self, ) @classmethod def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SharedUser": diff --git a/telegram/_user.py b/telegram/_user.py index 640a3573acc..63ce8fff850 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -34,6 +34,7 @@ ReplyMarkup, TimePeriod, ) +from telegram._utils.usernames import get_name, get_full_name, get_link from telegram.helpers import mention_html as helpers_mention_html from telegram.helpers import mention_markdown as helpers_mention_markdown @@ -207,27 +208,21 @@ def name(self) -> str: """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`. """ - if self.username: - return f"@{self.username}" - return self.full_name + return get_name(self, ) @property def full_name(self) -> str: """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if available) :attr:`last_name`. """ - if self.last_name: - return f"{self.first_name} {self.last_name}" - return self.first_name + return get_full_name(self, ) @property def link(self) -> Optional[str]: """:obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link of the user. """ - if self.username: - return f"https://t.me/{self.username}" - return None + return get_link(self, ) async def get_profile_photos( self, diff --git a/telegram/_utils/usernames.py b/telegram/_utils/usernames.py new file mode 100644 index 00000000000..fb1184efaad --- /dev/null +++ b/telegram/_utils/usernames.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 __future__ import annotations +import typing +"""This module contains auxiliary functionality for building strings for __repr__ method.""" + + +class UserLikeOptional(typing.Protocol): + """ + Note: + `User`, `Contact` (and maybe some other) objects always have first_name, + unlike the `Chat` and `Shared`, were they are optional. + The `last_name` is always optional. + """ + last_name: typing.Optional[str] + username: typing.Optional[str] + + +class UserLike(UserLikeOptional): + """ + Note: + `User`, `Contact` (and maybe some other) objects always have first_name, + unlike the `Chat` and `Shared`, were they are optional. + The `last_name` is always optional. + """ + first_name: str + + +class MiniUserLike(UserLikeOptional): + """ + Note: + `User`, `Contact` (and maybe some other) objects always have first_name, + unlike the `Chat` and `Shared`, were they are optional. + The `last_name` is always optional. + """ + first_name: typing.Optional[str] + + +@typing.overload +def get_name(user: UserLike) -> str: + ... + + +@typing.overload +def get_name(user: MiniUserLike) -> str | None: + ... + + +def get_name(user: UserLike | MiniUserLike) -> str | None: + """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` + prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`. + For the UserLike object str will always be returned as `first_name`always exists. + """ + if user.username: + return f"@{user.username}" + return get_full_name(user=user, ) + + +@typing.overload +def get_full_name(user: UserLike) -> str: + ... + +@typing.overload +def get_full_name(user: MiniUserLike) -> str | None: + ... + + +def get_full_name(user: UserLike | MiniUserLike) -> str | None: + """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if + available) :attr:`last_name`, otherwise None. + For the UserLike object str will always be returned as `first_name`always exists. + """ + if user.first_name and user.last_name: + return f"{user.first_name} {user.last_name}" + if user.first_name or user.last_name: + return f"{user.first_name or user.last_name}" + return None + + +def get_link(user: UserLike | MiniUserLike) -> str | None: + """:obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link + of the user. + """ + if user.username: + return f"https://t.me/{user.username}" + return None \ No newline at end of file diff --git a/tests/_utils/test_usernames.py b/tests/_utils/test_usernames.py new file mode 100644 index 00000000000..cad327cc36c --- /dev/null +++ b/tests/_utils/test_usernames.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# 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 __future__ import annotations +import pytest +from typing import TYPE_CHECKING +from telegram import SharedUser +from tests.test_user import user +from telegram._utils.usernames import get_name, get_full_name, get_link + +if TYPE_CHECKING: + from telegram._utils.usernames import UserLike, MiniUserLike + + +@pytest.fixture(scope="class") +def shared_user(): + result = SharedUser( + user_id=1, + first_name="first\u2022name", + last_name="last\u2022name", + username="username", + ) + result._unfreeze() + return result + + +def test_get_name(user, ): + assert get_name(user=user) == "@username" + user.username = None + assert get_name(user=user) == "first\u2022name last\u2022name" + + +def test_full_name_both_exists(user: UserLike, shared_user: MiniUserLike, ): + assert get_full_name(user=user) == get_full_name(user=shared_user) == "first\u2022name last\u2022name" + + +def test_full_name_last_name_missed(user: UserLike, shared_user: MiniUserLike, ): + user.last_name = shared_user.last_name = None + assert get_full_name(user=user) == get_full_name(user=shared_user) == "first\u2022name" + + +def test_full_name_first_name_missed(user: UserLike, shared_user: MiniUserLike, ): + user.first_name = shared_user.first_name = None + assert get_full_name(user=user) == get_full_name(user=shared_user) == "last\u2022name" + + +def test_full_name_both_missed(user: UserLike, shared_user: MiniUserLike, ): + user.first_name = user.last_name = shared_user.first_name = shared_user.last_name = None + assert get_full_name(user=user) is get_full_name(user=shared_user) is None + + +def test_link(user: UserLike, shared_user: MiniUserLike, ): + assert get_link(user=user, ) == get_link(user=shared_user, ) == f"https://t.me/{user.username}" + user.username = shared_user.username = None + assert get_link(user=user, ) is get_link(user=shared_user, ) is None diff --git a/tests/test_shared.py b/tests/test_shared.py index 61478b7a8aa..877eb69578d 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -231,31 +231,3 @@ def test_equality(self, chat_shared): assert a != d assert hash(a) != hash(d) - def test_name(self, shared_user, ): - with shared_user._unfrozen(): - assert shared_user.name == "@user" - with patch.object(shared_user, 'username', None): - assert shared_user.name == "first last" - with patch.multiple( - shared_user, - last_name=None, - first_name=None, - ): - assert shared_user.full_name is None - - def test_full_name(self, shared_user): - with shared_user._unfrozen(): - # Test `and` (both exists) - assert shared_user.full_name == "first last" - # Test `or` (one of them exists) - with patch.object(shared_user, 'first_name', None): - assert shared_user.full_name == "last" - with patch.object(shared_user, 'last_name', None): - assert shared_user.full_name == "first" - # Test None (None of them exists) - with patch.multiple( - shared_user, - last_name=None, - first_name=None, - ): - assert shared_user.full_name is None From f44f0184d0a9cd5e64ad2b9e8eb4fe568901bc9f Mon Sep 17 00:00:00 2001 From: david-shiko Date: Mon, 10 Mar 2025 04:30:38 +0300 Subject: [PATCH 3/4] Replace `Chat` `link` property implementation on `telegram._utils.usernames.py` implementation; Note: `full_name` and `effective_name` can not be easily replaced in the such way --- telegram/_chat.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/telegram/_chat.py b/telegram/_chat.py index fe49dc3593e..1ad7d2d229d 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -39,6 +39,7 @@ ReplyMarkup, TimePeriod, ) +from telegram._utils.usernames import get_link from telegram.helpers import escape_markdown from telegram.helpers import mention_html as helpers_mention_html from telegram.helpers import mention_markdown as helpers_mention_markdown @@ -162,9 +163,7 @@ def link(self) -> Optional[str]: """:obj:`str`: Convenience property. If the chat has a :attr:`~Chat.username`, returns a t.me link of the chat. """ - if self.username: - return f"https://t.me/{self.username}" - return None + return get_link(user=self) def mention_markdown(self, name: Optional[str] = None) -> str: """ From 0fc4c209db38fee4a27e802899649d488b337211 Mon Sep 17 00:00:00 2001 From: david-shiko Date: Mon, 10 Mar 2025 05:30:22 +0300 Subject: [PATCH 4/4] Adjust code according to pre-isntall hooks. Note: not all issues resolved; Slots tests failed for Protocol class. --- telegram/_shared.py | 2 +- telegram/_user.py | 2 +- telegram/_utils/usernames.py | 25 ++++++++++++------------- tests/_utils/test_usernames.py | 23 ++++++++++++----------- tests/test_shared.py | 3 --- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/telegram/_shared.py b/telegram/_shared.py index f22ccd1a4c7..1334625cb42 100644 --- a/telegram/_shared.py +++ b/telegram/_shared.py @@ -23,8 +23,8 @@ from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg -from telegram._utils.types import JSONDict from telegram._utils.usernames import get_name, get_full_name, get_link +from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram._bot import Bot diff --git a/telegram/_user.py b/telegram/_user.py index 63ce8fff850..1e04c59c953 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -26,6 +26,7 @@ from telegram._menubutton import MenuButton from telegram._telegramobject import TelegramObject from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.usernames import get_name, get_full_name, get_link from telegram._utils.types import ( CorrectOptionID, FileInput, @@ -34,7 +35,6 @@ ReplyMarkup, TimePeriod, ) -from telegram._utils.usernames import get_name, get_full_name, get_link from telegram.helpers import mention_html as helpers_mention_html from telegram.helpers import mention_markdown as helpers_mention_markdown diff --git a/telegram/_utils/usernames.py b/telegram/_utils/usernames.py index fb1184efaad..1f4e393c9d7 100644 --- a/telegram/_utils/usernames.py +++ b/telegram/_utils/usernames.py @@ -16,22 +16,20 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. - - +"""Shared properties to extract username, first_name, last_name values if filled.""" from __future__ import annotations -import typing -"""This module contains auxiliary functionality for building strings for __repr__ method.""" +from typing import Protocol, overload -class UserLikeOptional(typing.Protocol): +class UserLikeOptional(Protocol): """ Note: `User`, `Contact` (and maybe some other) objects always have first_name, unlike the `Chat` and `Shared`, were they are optional. The `last_name` is always optional. """ - last_name: typing.Optional[str] - username: typing.Optional[str] + last_name: str | None + username: str | None class UserLike(UserLikeOptional): @@ -51,15 +49,15 @@ class MiniUserLike(UserLikeOptional): unlike the `Chat` and `Shared`, were they are optional. The `last_name` is always optional. """ - first_name: typing.Optional[str] + first_name: str | None -@typing.overload +@overload def get_name(user: UserLike) -> str: ... -@typing.overload +@overload def get_name(user: MiniUserLike) -> str | None: ... @@ -74,11 +72,12 @@ def get_name(user: UserLike | MiniUserLike) -> str | None: return get_full_name(user=user, ) -@typing.overload +@overload def get_full_name(user: UserLike) -> str: ... -@typing.overload + +@overload def get_full_name(user: MiniUserLike) -> str | None: ... @@ -101,4 +100,4 @@ def get_link(user: UserLike | MiniUserLike) -> str | None: """ if user.username: return f"https://t.me/{user.username}" - return None \ No newline at end of file + return None diff --git a/tests/_utils/test_usernames.py b/tests/_utils/test_usernames.py index cad327cc36c..8f84e465d9a 100644 --- a/tests/_utils/test_usernames.py +++ b/tests/_utils/test_usernames.py @@ -21,7 +21,7 @@ import pytest from typing import TYPE_CHECKING from telegram import SharedUser -from tests.test_user import user +from tests.test_user import user # noqa: F401 noqa: F811 from telegram._utils.usernames import get_name, get_full_name, get_link if TYPE_CHECKING: @@ -40,32 +40,33 @@ def shared_user(): return result -def test_get_name(user, ): - assert get_name(user=user) == "@username" - user.username = None - assert get_name(user=user) == "first\u2022name last\u2022name" +def test_get_name(user: UserLike, shared_user: MiniUserLike, ): # noqa: F811 + assert get_name(user=user) == get_name(user=shared_user) == "@username" + shared_user.username = user.username = None + assert get_name(user=user) == get_name(user=shared_user) == "first\u2022name last\u2022name" -def test_full_name_both_exists(user: UserLike, shared_user: MiniUserLike, ): - assert get_full_name(user=user) == get_full_name(user=shared_user) == "first\u2022name last\u2022name" +def test_full_name_both_exists(user: UserLike, shared_user: MiniUserLike, ): # noqa: F811 + expected = "first\u2022name last\u2022name" + assert get_full_name(user=user) == get_full_name(user=shared_user) == expected -def test_full_name_last_name_missed(user: UserLike, shared_user: MiniUserLike, ): +def test_full_name_last_name_missed(user: UserLike, shared_user: MiniUserLike, ): # noqa: F811 user.last_name = shared_user.last_name = None assert get_full_name(user=user) == get_full_name(user=shared_user) == "first\u2022name" -def test_full_name_first_name_missed(user: UserLike, shared_user: MiniUserLike, ): +def test_full_name_first_name_missed(user: UserLike, shared_user: MiniUserLike, ): # noqa: F811 user.first_name = shared_user.first_name = None assert get_full_name(user=user) == get_full_name(user=shared_user) == "last\u2022name" -def test_full_name_both_missed(user: UserLike, shared_user: MiniUserLike, ): +def test_full_name_both_missed(user: UserLike, shared_user: MiniUserLike, ): # noqa: F811 user.first_name = user.last_name = shared_user.first_name = shared_user.last_name = None assert get_full_name(user=user) is get_full_name(user=shared_user) is None -def test_link(user: UserLike, shared_user: MiniUserLike, ): +def test_link(user: UserLike, shared_user: MiniUserLike, ): # noqa: F811 assert get_link(user=user, ) == get_link(user=shared_user, ) == f"https://t.me/{user.username}" user.username = shared_user.username = None assert get_link(user=user, ) is get_link(user=shared_user, ) is None diff --git a/tests/test_shared.py b/tests/test_shared.py index 877eb69578d..239e8600092 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -17,8 +17,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/]. -from unittest.mock import patch - import pytest from telegram import ChatShared, PhotoSize, SharedUser, UsersShared @@ -230,4 +228,3 @@ def test_equality(self, chat_shared): assert a != d assert hash(a) != hash(d) - pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy