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

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: 5 additions & 0 deletions changes/unreleased/4750.jJBu7iAgZa96hdqcpHK96W.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
other = "Use `timedelta` to represent time periods in classes"
[[pull_requests]]
uid = "4750"
author_uid = "aelkheir"
closes_threads = []
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.

.. |timespan-seconds-deprecated| replace:: In a future major version this will be of type :obj:`datetime.timedelta`. You can opt-in early by setting the `PTB_TIMEDELTA` environment variable.
93 changes: 74 additions & 19 deletions telegram/_chatfullinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,26 @@
"""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
from telegram._chatlocation import ChatLocation
from telegram._chatpermissions import ChatPermissions
from telegram._files.chatphoto import ChatPhoto
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_period_arg,
parse_sequence_arg,
)
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, BusinessIntro, BusinessLocation, BusinessOpeningHours, Message
Expand Down Expand Up @@ -155,17 +164,29 @@ 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|

.. deprecated:: NEXT.VERSION
|timespan-seconds-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

.. versionchanged:: NEXT.VERSION
|time-period-input|

.. deprecated:: NEXT.VERSION
|timespan-seconds-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 @@ -312,17 +333,29 @@ 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|

.. deprecated:: NEXT.VERSION
|timespan-seconds-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

.. versionchanged:: NEXT.VERSION
|time-period-input|

.. deprecated:: NEXT.VERSION
|timespan-seconds-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 @@ -366,6 +399,8 @@ class ChatFullInfo(_ChatBase):
"""

__slots__ = (
"_message_auto_delete_time",
"_slow_mode_delay",
"accent_color_id",
"active_usernames",
"available_reactions",
Expand Down Expand Up @@ -394,14 +429,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 @@ -439,9 +472,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 @@ -477,9 +510,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] = parse_period_arg(slow_mode_delay)
self._message_auto_delete_time: Optional[dtm.timedelta] = parse_period_arg(
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 @@ -520,6 +553,14 @@ def __init__(
self.can_send_paid_media: Optional[bool] = can_send_paid_media
self.can_send_gift: Optional[bool] = can_send_gift

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

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

@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
"""See :meth:`telegram.TelegramObject.de_json`."""
Expand All @@ -541,6 +582,13 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
Message,
)

data["slow_mode_delay"] = (
dtm.timedelta(seconds=s) if (s := data.get("slow_mode_delay")) else None
)
data["message_auto_delete_time"] = (
dtm.timedelta(seconds=s) if (s := data.get("message_auto_delete_time")) else None
)

data["pinned_message"] = de_json_optional(data.get("pinned_message"), Message, bot)
data["permissions"] = de_json_optional(data.get("permissions"), ChatPermissions, bot)
data["location"] = de_json_optional(data.get("location"), ChatLocation, bot)
Expand All @@ -558,3 +606,10 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
)

return super().de_json(data=data, bot=bot)

def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
out = super().to_dict(recursive)
out["slow_mode_delay"] = self.slow_mode_delay
out["message_auto_delete_time"] = self.message_auto_delete_time
return out
31 changes: 29 additions & 2 deletions telegram/_utils/argumentparsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""
import datetime as dtm
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional, Protocol, TypeVar
from typing import TYPE_CHECKING, Optional, Protocol, TypeVar, Union

from telegram._linkpreviewoptions import LinkPreviewOptions
from telegram._telegramobject import TelegramObject
from telegram._utils.types import JSONDict, ODVInput
from telegram._utils.types import JSONDict, ODVInput, TimePeriod
from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning

if TYPE_CHECKING:
from typing import type_check_only
Expand All @@ -50,6 +53,30 @@ def parse_sequence_arg(arg: Optional[Sequence[T]]) -> tuple[T, ...]:
return tuple(arg) if arg else ()


def parse_period_arg(arg: Optional[TimePeriod]) -> Union[dtm.timedelta, None]:
"""Parses an optional time period in seconds into a timedelta

Args:
arg (:obj:`int` | :class:`datetime.timedelta`, optional): The time period to parse.

Returns:
:obj:`timedelta`: The time period converted to a timedelta object or :obj:`None`.
"""
if arg is None:
return None
if isinstance(arg, (int, float)):
warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"In a future major version this will be of type `datetime.timedelta`."
" You can opt-in early by setting the `PTB_TIMEDELTA` environment variable.",
),
stacklevel=2,
)
return dtm.timedelta(seconds=arg)
return arg


def parse_lpo_and_dwpp(
disable_web_page_preview: Optional[bool], link_preview_options: ODVInput[LinkPreviewOptions]
) -> ODVInput[LinkPreviewOptions]:
Expand Down
27 changes: 27 additions & 0 deletions telegram/_utils/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@
"""
import contextlib
import datetime as dtm
import os
import time
from typing import TYPE_CHECKING, Optional, Union

from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.envvars import env_var_2_bool

if TYPE_CHECKING:
from telegram import Bot

Expand Down Expand Up @@ -224,3 +229,25 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
if dt_obj.tzinfo is None:
dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc)
return dt_obj.timestamp()


def get_timedelta_value(value: Optional[dtm.timedelta]) -> Optional[Union[int, dtm.timedelta]]:
"""
Convert a `datetime.timedelta` to seconds or return it as-is, based on environment config.
"""
if value is None:
return None

if env_var_2_bool(os.getenv("PTB_TIMEDELTA")):
return value

warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"In a future major version this will be of type `datetime.timedelta`."
" You can opt-in early by setting the `PTB_TIMEDELTA` environment variable.",
),
stacklevel=2,
)
# We don't want to silently drop fractions, so float is returned and we slience mypy
return value.total_seconds() # type: ignore[return-value]
20 changes: 19 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
import asyncio
import logging
import os
import sys
import zoneinfo
from pathlib import Path
Expand All @@ -40,7 +41,12 @@
from tests.auxil.build_messages import DATE, make_message
from tests.auxil.ci_bots import BOT_INFO_PROVIDER, JOB_INDEX
from tests.auxil.constants import PRIVATE_KEY, TEST_TOPIC_ICON_COLOR, TEST_TOPIC_NAME
from tests.auxil.envvars import GITHUB_ACTIONS, RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS
from tests.auxil.envvars import (
GITHUB_ACTIONS,
RUN_TEST_OFFICIAL,
TEST_WITH_OPT_DEPS,
env_var_2_bool,
)
from tests.auxil.files import data_file
from tests.auxil.networking import NonchalantHttpxRequest
from tests.auxil.pytest_classes import PytestBot, make_bot
Expand Down Expand Up @@ -129,6 +135,18 @@ def _disallow_requests_in_without_request_tests(request):
)


@pytest.fixture(scope="module", params=["true", "false", None])
def PTB_TIMEDELTA(request):
# Here we manually use monkeypatch to give this fixture module scope
monkeypatch = pytest.MonkeyPatch()
if request.param is not None:
monkeypatch.setenv("PTB_TIMEDELTA", request.param)
else:
monkeypatch.delenv("PTB_TIMEDELTA", raising=False)
yield env_var_2_bool(os.getenv("PTB_TIMEDELTA"))
monkeypatch.undo()


# Redefine the event_loop fixture to have a session scope. Otherwise `bot` fixture can't be
# session. See https://github.com/pytest-dev/pytest-asyncio/issues/68 for more details.
@pytest.fixture(scope="session")
Expand Down
Loading
Loading