Thanks to visit codestin.com
Credit goes to github.com

Skip to content

User like properties #4713

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions telegram/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down
24 changes: 23 additions & 1 deletion telegram/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
# 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
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
from telegram._utils.usernames import get_name, get_full_name, get_link
from telegram._utils.types import JSONDict

if TYPE_CHECKING:
Expand Down Expand Up @@ -244,6 +245,27 @@ 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`.
"""
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.
"""
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":
"""See :meth:`telegram.TelegramObject.de_json`."""
Expand Down
13 changes: 4 additions & 9 deletions telegram/_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
103 changes: 103 additions & 0 deletions telegram/_utils/usernames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <[email protected]>
#
# 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/].
"""Shared properties to extract username, first_name, last_name values if filled."""
from __future__ import annotations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please avoid using this. If this is for using |, please use Union and Optional instead

from typing import Protocol, overload


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: str | None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
last_name: str | None
first_name: Optional[str]
last_name: str | None

that way you should be able to remove MiniUserLike

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. One of the hooks, probably black or isort, converts automatically during commit.
  2. Sometimes, the field always exists (for example, for the User type) and sometimes it might not (like in the SharedUser type). This affects the return type: it can be Optional[str] or guaranteed to be an str . That's why I introduced it.
  3. I know the name MiniUserLike is pretty silly, but I couldn't think of anything better. I hope you can help me with that :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • running the pre-commit hooks on python 3.9 (lowest supported version) should make sure that this doesn't happen. Though I admit I'm not completely sure why it does in the first place
  • you already differentiate between UserLikeOptional and UserLike. That should already do the trick IMO

username: str | None


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: str | None


@overload
def get_name(user: UserLike) -> str:
...


@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, )


@overload
def get_full_name(user: UserLike) -> str:
...


@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
72 changes: 72 additions & 0 deletions tests/_utils/test_usernames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# Leandro Toledo de Souza <[email protected]>
#
# 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 # noqa: F401 noqa: F811
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: 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, ): # 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, ): # 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, ): # 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, ): # 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, ): # 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
Loading