Skip to content

Use timedelta to represent time periods in classes #4750

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

Merged
merged 33 commits into from
Jun 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
40d354a
Setup helper functions and a common test fixture.
aelkheir Apr 8, 2025
5cd6507
Accept timedeltas in params of `ChatFullInfo`.
aelkheir Apr 8, 2025
a3b340b
Add chango fragment for PR #4750
aelkheir Apr 9, 2025
d60b9fe
Refactor `test_chatfullinfo.py` a bit.
aelkheir Apr 9, 2025
d4d62ce
Oops, so many white spaces.
aelkheir Apr 9, 2025
3084d3b
Finish up `ChatFullInfo` plus some helper tweaks.
aelkheir Apr 11, 2025
8d48756
Modify ``docs/substitutions/global.rst``.
aelkheir Apr 11, 2025
8ef4ca9
Accept timedeltas in ``duration`` param of media classes.
aelkheir Apr 12, 2025
28af9f7
Undeprecate passing `ints` to classes' arguments.
aelkheir May 15, 2025
319dd6c
Accept timedeltas in params of `InlineQueryResult.*` classes.
aelkheir May 16, 2025
d8dbe15
Accept timedeltas in other time period params.
aelkheir May 17, 2025
caa831b
Modify test_official to handle time periods as timedelta automatically.
aelkheir May 17, 2025
881a9f6
Accept timedeltas in Bot.get_updates.timeout.
aelkheir May 17, 2025
466e0b0
Elaborate chango fragment for PR.
aelkheir May 17, 2025
216b90b
Merge remote-tracking branch 'upstream/master' into feature/4575-time…
aelkheir May 20, 2025
fb9a709
Update ``timeout`` type annotation in Application, Updater methods.
aelkheir May 20, 2025
4e9f5fa
Accept timedelta in RetryAfter.
aelkheir May 23, 2025
a4d4d12
Include attribute name in warning message.
aelkheir May 23, 2025
3194848
review: refactor `_utils/datetime.get_timedelta_value`.
aelkheir May 23, 2025
de83ac6
Remove temporarily time period parser introduced in #4769.
aelkheir May 23, 2025
10e716a
review: address comments
aelkheir May 24, 2025
b20f9f9
Fix precommit and update `test_request.py`.
aelkheir May 24, 2025
719e9b3
Fix a test in `test_updater.py` that hangs.
aelkheir May 24, 2025
cd0fd0e
Merge remote-tracking branch 'upstream/master' into feature/4575-time…
aelkheir May 26, 2025
574b09d
Move `to_dict` logic to `_telegramobject.py`.
aelkheir May 26, 2025
2a4ed74
Merge remote-tracking branch 'upstream/master' into feature/4575-time…
aelkheir May 28, 2025
eb34ae3
Add a test for `get_timedelta_value`.
aelkheir May 28, 2025
7d93eb5
Fix precommit.
aelkheir Jun 1, 2025
aba92df
review: address comments.
aelkheir Jun 9, 2025
11ac715
review: update handling of deprecation logic in telegramobject.
aelkheir Jun 9, 2025
e9a63c2
fix typo in docstring of `TO._is_deprecated_attr`
aelkheir Jun 9, 2025
0ef3041
review: some other points.
aelkheir Jun 16, 2025
5aabf59
Mock business float period properties.
aelkheir Jun 28, 2025
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
36 changes: 36 additions & 0 deletions changes/unreleased/4750.jJBu7iAgZa96hdqcpHK96W.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
features = "Use `timedelta` to represent time periods in class arguments and attributes"
deprecations = """In this release, we're migrating attributes of Telegram objects that represent durations/time periods from having :obj:`int` type to Python's native :class:`datetime.timedelta`. This change is opt-in for now to allow for a smooth transition phase. It will become opt-out in future releases.

Set ``PTB_TIMEDELTA=true`` or ``PTB_TIMEDELTA=1`` as an environment variable to make these attributes return :obj:`datetime.timedelta` objects instead of integers. Support for :obj:`int` values is deprecated and will be removed in a future major version.

Affected Attributes:
- :attr:`telegram.ChatFullInfo.slow_mode_delay` and :attr:`telegram.ChatFullInfo.message_auto_delete_time`
- :attr:`telegram.Animation.duration`
- :attr:`telegram.Audio.duration`
- :attr:`telegram.Video.duration` and :attr:`telegram.Video.start_timestamp`
- :attr:`telegram.VideoNote.duration`
- :attr:`telegram.Voice.duration`
- :attr:`telegram.PaidMediaPreview.duration`
- :attr:`telegram.VideoChatEnded.duration`
- :attr:`telegram.InputMediaVideo.duration`
- :attr:`telegram.InputMediaAnimation.duration`
- :attr:`telegram.InputMediaAudio.duration`
- :attr:`telegram.InputPaidMediaVideo.duration`
- :attr:`telegram.InlineQueryResultGif.gif_duration`
- :attr:`telegram.InlineQueryResultMpeg4Gif.mpeg4_duration`
- :attr:`telegram.InlineQueryResultVideo.video_duration`
- :attr:`telegram.InlineQueryResultAudio.audio_duration`
- :attr:`telegram.InlineQueryResultVoice.voice_duration`
- :attr:`telegram.InlineQueryResultLocation.live_period`
- :attr:`telegram.Poll.open_period`
- :attr:`telegram.Location.live_period`
- :attr:`telegram.MessageAutoDeleteTimerChanged.message_auto_delete_time`
- :attr:`telegram.ChatInviteLink.subscription_period`
- :attr:`telegram.InputLocationMessageContent.live_period`
- :attr:`telegram.error.RetryAfter.retry_after`
"""
internal = "Modify `test_official` to handle time periods as timedelta automatically."
[[pull_requests]]
uid = "4750"
author_uid = "aelkheir"
closes_threads = ["4575"]
2 changes: 2 additions & 0 deletions docs/substitutions/global.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,5 @@
.. |org-verify| replace:: `on behalf of the organization <https://telegram.org/verify#third-party-verification>`__

.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.

.. |time-period-int-deprecated| replace:: In a future major version this attribute will be of type :obj:`datetime.timedelta`. You can opt-in early by setting `PTB_TIMEDELTA=true` or ``PTB_TIMEDELTA=1`` as an environment variable.
5 changes: 4 additions & 1 deletion examples/rawapibot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""
import asyncio
import contextlib
import datetime as dtm
import logging
from typing import NoReturn

Expand Down Expand Up @@ -47,7 +48,9 @@ async def main() -> NoReturn:
async def echo(bot: Bot, update_id: int) -> int:
"""Echo the message the user sent."""
# Request updates after the last update_id
updates = await bot.get_updates(offset=update_id, timeout=10, allowed_updates=Update.ALL_TYPES)
updates = await bot.get_updates(
offset=update_id, timeout=dtm.timedelta(seconds=10), allowed_updates=Update.ALL_TYPES
)
for update in updates:
next_update_id = update.update_id + 1

Expand Down
19 changes: 14 additions & 5 deletions src/telegram/_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4519,7 +4519,7 @@ async def get_updates(
self,
offset: Optional[int] = None,
limit: Optional[int] = None,
timeout: Optional[int] = None,
timeout: Optional[TimePeriod] = None,
allowed_updates: Optional[Sequence[str]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
Expand Down Expand Up @@ -4554,9 +4554,12 @@ async def get_updates(
between :tg-const:`telegram.constants.PollingLimit.MIN_LIMIT`-
:tg-const:`telegram.constants.PollingLimit.MAX_LIMIT` are accepted.
Defaults to ``100``.
timeout (:obj:`int`, optional): Timeout in seconds for long polling. Defaults to ``0``,
i.e. usual short polling. Should be positive, short polling should be used for
testing purposes only.
timeout (:obj:`int` | :class:`datetime.timedelta`, optional): Timeout in seconds for
long polling. Defaults to ``0``, i.e. usual short polling. Should be positive,
short polling should be used for testing purposes only.

.. versionchanged:: NEXT.VERSION
|time-period-input|
allowed_updates (Sequence[:obj:`str`]), optional): A sequence the types of
updates you want your bot to receive. For example, specify ["message",
"edited_channel_post", "callback_query"] to only receive updates of these types.
Expand Down Expand Up @@ -4591,6 +4594,12 @@ async def get_updates(
else:
arg_read_timeout = self._request[0].read_timeout or 0

read_timeout = (
(arg_read_timeout + timeout.total_seconds())
if isinstance(timeout, dtm.timedelta)
else (arg_read_timeout + timeout if timeout else arg_read_timeout)
)

# Ideally we'd use an aggressive read timeout for the polling. However,
# * Short polling should return within 2 seconds.
# * Long polling poses a different problem: the connection might have been dropped while
Expand All @@ -4601,7 +4610,7 @@ async def get_updates(
await self._post(
"getUpdates",
data,
read_timeout=arg_read_timeout + timeout if timeout else arg_read_timeout,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
Expand Down
69 changes: 50 additions & 19 deletions src/telegram/_chatfullinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"""This module contains an object that represents a Telegram ChatFullInfo."""
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Union

from telegram._birthdate import Birthdate
from telegram._chat import Chat, _ChatBase
Expand All @@ -29,9 +29,18 @@
from telegram._files.chatphoto import ChatPhoto
from telegram._gifts import AcceptedGiftTypes
from telegram._reaction import ReactionType
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
from telegram._utils.argumentparsing import (
de_json_optional,
de_list_optional,
parse_sequence_arg,
to_timedelta,
)
from telegram._utils.datetime import (
extract_tzinfo_from_defaults,
from_timestamp,
get_timedelta_value,
)
from telegram._utils.types import JSONDict, TimePeriod
from telegram._utils.warnings import warn
from telegram._utils.warnings_transition import (
build_deprecation_warning_message,
Expand Down Expand Up @@ -166,17 +175,23 @@ class ChatFullInfo(_ChatBase):
(by sending date).
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
for groups and supergroups.
slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between
consecutive messages sent by each unprivileged user.
slow_mode_delay (:obj:`int` | :class:`datetime.timedelta`, optional): For supergroups,
the minimum allowed delay between consecutive messages sent by each unprivileged user.

.. versionchanged:: NEXT.VERSION
|time-period-input|
unrestrict_boost_count (:obj:`int`, optional): For supergroups, the minimum number of
boosts that a non-administrator user needs to add in order to ignore slow mode and chat
permissions.

.. versionadded:: 21.0
message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to
the chat will be automatically deleted; in seconds.
message_auto_delete_time (:obj:`int` | :class:`datetime.timedelta`, optional): The time
after which all messages sent to the chat will be automatically deleted; in seconds.

.. versionadded:: 13.4

.. versionchanged:: NEXT.VERSION
|time-period-input|
has_aggressive_anti_spam_enabled (:obj:`bool`, optional): :obj:`True`, if aggressive
anti-spam checks are enabled in the supergroup. The field is only available to chat
administrators.
Expand Down Expand Up @@ -331,17 +346,23 @@ class ChatFullInfo(_ChatBase):
(by sending date).
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
for groups and supergroups.
slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between
consecutive messages sent by each unprivileged user.
slow_mode_delay (:obj:`int` | :class:`datetime.timedelta`): Optional. For supergroups,
the minimum allowed delay between consecutive messages sent by each unprivileged user.

.. deprecated:: NEXT.VERSION
|time-period-int-deprecated|
unrestrict_boost_count (:obj:`int`): Optional. For supergroups, the minimum number of
boosts that a non-administrator user needs to add in order to ignore slow mode and chat
permissions.

.. versionadded:: 21.0
message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to
the chat will be automatically deleted; in seconds.
message_auto_delete_time (:obj:`int` | :class:`datetime.timedelta`): Optional. The time
after which all messages sent to the chat will be automatically deleted; in seconds.

.. versionadded:: 13.4

.. deprecated:: NEXT.VERSION
|time-period-int-deprecated|
has_aggressive_anti_spam_enabled (:obj:`bool`): Optional. :obj:`True`, if aggressive
anti-spam checks are enabled in the supergroup. The field is only available to chat
administrators.
Expand Down Expand Up @@ -383,6 +404,8 @@ class ChatFullInfo(_ChatBase):

__slots__ = (
"_can_send_gift",
"_message_auto_delete_time",
"_slow_mode_delay",
"accent_color_id",
"accepted_gift_types",
"active_usernames",
Expand Down Expand Up @@ -411,14 +434,12 @@ class ChatFullInfo(_ChatBase):
"linked_chat_id",
"location",
"max_reaction_count",
"message_auto_delete_time",
"permissions",
"personal_chat",
"photo",
"pinned_message",
"profile_accent_color_id",
"profile_background_custom_emoji_id",
"slow_mode_delay",
"sticker_set_name",
"unrestrict_boost_count",
)
Expand Down Expand Up @@ -456,9 +477,9 @@ def __init__(
invite_link: Optional[str] = None,
pinned_message: Optional["Message"] = None,
permissions: Optional[ChatPermissions] = None,
slow_mode_delay: Optional[int] = None,
slow_mode_delay: Optional[TimePeriod] = None,
unrestrict_boost_count: Optional[int] = None,
message_auto_delete_time: Optional[int] = None,
message_auto_delete_time: Optional[TimePeriod] = None,
has_aggressive_anti_spam_enabled: Optional[bool] = None,
has_hidden_members: Optional[bool] = None,
has_protected_content: Optional[bool] = None,
Expand Down Expand Up @@ -513,9 +534,9 @@ def __init__(
self.invite_link: Optional[str] = invite_link
self.pinned_message: Optional[Message] = pinned_message
self.permissions: Optional[ChatPermissions] = permissions
self.slow_mode_delay: Optional[int] = slow_mode_delay
self.message_auto_delete_time: Optional[int] = (
int(message_auto_delete_time) if message_auto_delete_time is not None else None
self._slow_mode_delay: Optional[dtm.timedelta] = to_timedelta(slow_mode_delay)
self._message_auto_delete_time: Optional[dtm.timedelta] = to_timedelta(
message_auto_delete_time
)
self.has_protected_content: Optional[bool] = has_protected_content
self.has_visible_history: Optional[bool] = has_visible_history
Expand Down Expand Up @@ -576,6 +597,16 @@ def can_send_gift(self) -> Optional[bool]:
)
return self._can_send_gift

@property
def slow_mode_delay(self) -> Optional[Union[int, dtm.timedelta]]:
return get_timedelta_value(self._slow_mode_delay, attribute="slow_mode_delay")

@property
def message_auto_delete_time(self) -> Optional[Union[int, dtm.timedelta]]:
return get_timedelta_value(
self._message_auto_delete_time, attribute="message_auto_delete_time"
)

@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
"""See :meth:`telegram.TelegramObject.de_json`."""
Expand Down
36 changes: 25 additions & 11 deletions src/telegram/_chatinvitelink.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents an invite link for a chat."""
import datetime as dtm
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Union

from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
from telegram._utils.argumentparsing import de_json_optional, to_timedelta
from telegram._utils.datetime import (
extract_tzinfo_from_defaults,
from_timestamp,
get_timedelta_value,
)
from telegram._utils.types import JSONDict, TimePeriod

if TYPE_CHECKING:
from telegram import Bot
Expand Down Expand Up @@ -70,10 +74,13 @@ class ChatInviteLink(TelegramObject):
created using this link.

.. versionadded:: 13.8
subscription_period (:obj:`int`, optional): The number of seconds the subscription will be
active for before the next payment.
subscription_period (:obj:`int` | :class:`datetime.timedelta`, optional): The number of
seconds the subscription will be active for before the next payment.

.. versionadded:: 21.5

.. versionchanged:: NEXT.VERSION
|time-period-input|
subscription_price (:obj:`int`, optional): The amount of Telegram Stars a user must pay
initially and after each subsequent subscription period to be a member of the chat
using the link.
Expand Down Expand Up @@ -107,10 +114,13 @@ class ChatInviteLink(TelegramObject):
created using this link.

.. versionadded:: 13.8
subscription_period (:obj:`int`): Optional. The number of seconds the subscription will be
active for before the next payment.
subscription_period (:obj:`int` | :class:`datetime.timedelta`): Optional. The number of
seconds the subscription will be active for before the next payment.

.. versionadded:: 21.5

.. deprecated:: NEXT.VERSION
|time-period-int-deprecated|
subscription_price (:obj:`int`): Optional. The amount of Telegram Stars a user must pay
initially and after each subsequent subscription period to be a member of the chat
using the link.
Expand All @@ -120,6 +130,7 @@ class ChatInviteLink(TelegramObject):
"""

__slots__ = (
"_subscription_period",
"creates_join_request",
"creator",
"expire_date",
Expand All @@ -129,7 +140,6 @@ class ChatInviteLink(TelegramObject):
"member_limit",
"name",
"pending_join_request_count",
"subscription_period",
"subscription_price",
)

Expand All @@ -144,7 +154,7 @@ def __init__(
member_limit: Optional[int] = None,
name: Optional[str] = None,
pending_join_request_count: Optional[int] = None,
subscription_period: Optional[int] = None,
subscription_period: Optional[TimePeriod] = None,
subscription_price: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
Expand All @@ -164,7 +174,7 @@ def __init__(
self.pending_join_request_count: Optional[int] = (
int(pending_join_request_count) if pending_join_request_count is not None else None
)
self.subscription_period: Optional[int] = subscription_period
self._subscription_period: Optional[dtm.timedelta] = to_timedelta(subscription_period)
self.subscription_price: Optional[int] = subscription_price

self._id_attrs = (
Expand All @@ -177,6 +187,10 @@ def __init__(

self._freeze()

@property
def subscription_period(self) -> Optional[Union[int, dtm.timedelta]]:
return get_timedelta_value(self._subscription_period, attribute="subscription_period")

@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatInviteLink":
"""See :meth:`telegram.TelegramObject.de_json`."""
Expand Down
15 changes: 3 additions & 12 deletions src/telegram/_files/_inputstorycontent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from telegram._files.inputfile import InputFile
from telegram._telegramobject import TelegramObject
from telegram._utils import enum
from telegram._utils.argumentparsing import to_timedelta
from telegram._utils.files import parse_file_input
from telegram._utils.types import FileInput, JSONDict

Expand Down Expand Up @@ -158,18 +159,8 @@ def __init__(

with self._unfrozen():
self.video: Union[str, InputFile] = self._parse_file_input(video)
self.duration: Optional[dtm.timedelta] = self._parse_period_arg(duration)
self.cover_frame_timestamp: Optional[dtm.timedelta] = self._parse_period_arg(
self.duration: Optional[dtm.timedelta] = to_timedelta(duration)
self.cover_frame_timestamp: Optional[dtm.timedelta] = to_timedelta(
cover_frame_timestamp
)
self.is_animation: Optional[bool] = is_animation

# This helper is temporarly here until we can use `argumentparsing.parse_period_arg`
# from https://github.com/python-telegram-bot/python-telegram-bot/pull/4750
@staticmethod
def _parse_period_arg(arg: Optional[Union[float, dtm.timedelta]]) -> Optional[dtm.timedelta]:
if arg is None:
return None
if isinstance(arg, dtm.timedelta):
return arg
return dtm.timedelta(seconds=arg)
Loading
Loading