Bump APS & Deprecate pytz Support (#4582)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
This commit is contained in:
dependabot[bot] 2025-01-01 19:43:52 +01:00 committed by GitHub
parent 5f35304e63
commit d0a6e5141c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 190 additions and 99 deletions

View file

@ -64,7 +64,8 @@ jobs:
# Test the rest
export TEST_WITH_OPT_DEPS='true'
pip install .[all]
# need to manually install pytz here, because it's no longer in the optional reqs
pip install .[all] pytz
# `-n auto --dist worksteal` uses pytest-xdist to run tests on multiple CPU
# workers. Increasing number of workers has little effect on test duration, but it seems
# to increase flakyness.

View file

@ -158,7 +158,7 @@ PTB can be installed with optional dependencies:
* ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1,<1.3 <https://aiolimiter.readthedocs.io/en/stable/>`_. Use this, if you want to use ``telegram.ext.AIORateLimiter``.
* ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 <https://www.tornadoweb.org/en/stable/>`_ library. Use this, if you want to use ``telegram.ext.Updater.start_webhook``/``telegram.ext.Application.run_webhook``.
* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<5.6.0 <https://cachetools.readthedocs.io/en/latest/>`_ library. Use this, if you want to use `arbitrary callback_data <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Arbitrary-callback_data>`_.
* ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 <https://apscheduler.readthedocs.io/en/3.x/>`_ library and enforces `pytz>=2018.6 <https://pypi.org/project/pytz/>`_, where ``pytz`` is a dependency of ``APScheduler``. Use this, if you want to use the ``telegram.ext.JobQueue``.
* ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 <https://apscheduler.readthedocs.io/en/3.x/>`_ library. Use this, if you want to use the ``telegram.ext.JobQueue``.
To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot[socks,webhooks]"``.

View file

@ -76,9 +76,7 @@ http2 = [
]
job-queue = [
# APS doesn't have a strict stability policy. Let's be cautious for now.
"APScheduler~=3.10.4",
# pytz is required by APS and just needs the lower bound due to #2120
"pytz>=2018.6",
"APScheduler>=3.10.4,<3.12.0",
]
passport = [
"cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1",

View file

@ -16,4 +16,8 @@ pytest-xdist==3.6.1
flaky>=3.8.1
# used in test_official for parsing tg docs
beautifulsoup4
beautifulsoup4
# For testing with timezones. Might not be needed on all systems, but to ensure that unit tests
# run correctly on all systems, we include it here.
tzdata

View file

@ -27,6 +27,7 @@ Warning:
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""
import contextlib
import datetime as dtm
import time
from typing import TYPE_CHECKING, Optional, Union
@ -34,22 +35,26 @@ from typing import TYPE_CHECKING, Optional, Union
if TYPE_CHECKING:
from telegram import Bot
# pytz is only available if it was installed as dependency of APScheduler, so we make a little
# workaround here
DTM_UTC = dtm.timezone.utc
UTC = dtm.timezone.utc
try:
import pytz
UTC = pytz.utc
except ImportError:
UTC = DTM_UTC # type: ignore[assignment]
pytz = None # type: ignore[assignment]
def _localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime:
"""Localize the datetime, where UTC is handled depending on whether pytz is available or not"""
if tzinfo is DTM_UTC:
return datetime.replace(tzinfo=DTM_UTC)
return tzinfo.localize(datetime) # type: ignore[attr-defined]
def localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime:
"""Localize the datetime, both for pytz and zoneinfo timezones."""
if tzinfo is UTC:
return datetime.replace(tzinfo=UTC)
with contextlib.suppress(AttributeError):
# Since pytz might not be available, we need the suppress context manager
if isinstance(tzinfo, pytz.BaseTzInfo):
return tzinfo.localize(datetime)
if datetime.tzinfo is None:
return datetime.replace(tzinfo=tzinfo)
return datetime.astimezone(tzinfo)
def to_float_timestamp(
@ -87,7 +92,7 @@ def to_float_timestamp(
will be raised.
tzinfo (:class:`datetime.tzinfo`, optional): If :paramref:`time_object` is a naive object
from the :mod:`datetime` module, it will be interpreted as this timezone. Defaults to
``pytz.utc``, if available, and :attr:`datetime.timezone.utc` otherwise.
:attr:`datetime.timezone.utc` otherwise.
Note:
Only to be used by ``telegram.ext``.
@ -121,6 +126,12 @@ def to_float_timestamp(
return reference_timestamp + time_object
if tzinfo is None:
# We do this here rather than in the signature to ensure that we can make calls like
# to_float_timestamp(
# time, tzinfo=bot.defaults.tzinfo if bot.defaults else None
# )
# This ensures clean separation of concerns, i.e. the default timezone should not be
# the responsibility of the caller
tzinfo = UTC
if isinstance(time_object, dtm.time):
@ -132,7 +143,9 @@ def to_float_timestamp(
aware_datetime = dtm.datetime.combine(reference_date, time_object)
if aware_datetime.tzinfo is None:
aware_datetime = _localize(aware_datetime, tzinfo)
# datetime.combine uses the tzinfo of `time_object`, which might be None
# so we still need to localize
aware_datetime = localize(aware_datetime, tzinfo)
# if the time of day has passed today, use tomorrow
if reference_time > aware_datetime.timetz():
@ -140,7 +153,7 @@ def to_float_timestamp(
return _datetime_to_float_timestamp(aware_datetime)
if isinstance(time_object, dtm.datetime):
if time_object.tzinfo is None:
time_object = _localize(time_object, tzinfo)
time_object = localize(time_object, tzinfo)
return _datetime_to_float_timestamp(time_object)
raise TypeError(f"Unable to convert {type(time_object).__name__} object to timestamp")

View file

@ -57,10 +57,13 @@ class Defaults:
versions.
tzinfo (:class:`datetime.tzinfo`, optional): A timezone to be used for all date(time)
inputs appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
somewhere, it will be assumed to be in :paramref:`tzinfo`. If the
:class:`telegram.ext.JobQueue` is used, this must be a timezone provided
by the ``pytz`` module. Defaults to ``pytz.utc``, if available, and
somewhere, it will be assumed to be in :paramref:`tzinfo`. Defaults to
:attr:`datetime.timezone.utc` otherwise.
.. deprecated:: NEXT.VERSION
Support for ``pytz`` timezones is deprecated and will be removed in future
versions.
block (:obj:`bool`, optional): Default setting for the :paramref:`BaseHandler.block`
parameter
of handlers and error handlers registered through :meth:`Application.add_handler` and
@ -148,6 +151,19 @@ class Defaults:
self._block: bool = block
self._protect_content: Optional[bool] = protect_content
if "pytz" in str(self._tzinfo.__class__):
# TODO: When dropping support, make sure to update _utils.datetime accordingly
warn(
message=PTBDeprecationWarning(
version="NEXT.VERSION",
message=(
"Support for pytz timezones is deprecated and will be removed in "
"future versions."
),
),
stacklevel=2,
)
if disable_web_page_preview is not None and link_preview_options is not None:
raise ValueError(
"`disable_web_page_preview` and `link_preview_options` are mutually exclusive."

View file

@ -23,7 +23,6 @@ import weakref
from typing import TYPE_CHECKING, Any, Generic, Optional, Union, cast, overload
try:
import pytz
from apscheduler.executors.asyncio import AsyncIOExecutor
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@ -31,6 +30,7 @@ try:
except ImportError:
APS_AVAILABLE = False
from telegram._utils.datetime import UTC, localize
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import JSONDict
@ -155,13 +155,13 @@ class JobQueue(Generic[CCT]):
dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary.
"""
timezone: object = pytz.utc
timezone: dtm.tzinfo = UTC
if (
self._application
and isinstance(self.application.bot, ExtBot)
and self.application.bot.defaults
):
timezone = self.application.bot.defaults.tzinfo or pytz.utc
timezone = self.application.bot.defaults.tzinfo or UTC
return {
"timezone": timezone,
@ -197,8 +197,10 @@ class JobQueue(Generic[CCT]):
dtm.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
)
if date_time.tzinfo is None:
date_time = self.scheduler.timezone.localize(date_time)
if shift_day and date_time <= dtm.datetime.now(pytz.utc):
# dtm.combine uses the tzinfo of `time`, which might be None, so we still have
# to localize it
date_time = localize(date_time, self.scheduler.timezone)
if shift_day and date_time <= dtm.datetime.now(UTC):
date_time += dtm.timedelta(days=1)
return date_time
return time

View file

@ -18,6 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
import datetime as dtm
import time
import zoneinfo
import pytest
@ -55,18 +56,38 @@ with the TEST_WITH_OPT_DEPS=False environment variable in addition to the regula
class TestDatetime:
@staticmethod
def localize(dt, tzinfo):
if TEST_WITH_OPT_DEPS:
return tzinfo.localize(dt)
return dt.replace(tzinfo=tzinfo)
def test_localize_utc(self):
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
localized_dt = tg_dtm.localize(dt, tg_dtm.UTC)
assert localized_dt.tzinfo == tg_dtm.UTC
assert localized_dt == dt.replace(tzinfo=tg_dtm.UTC)
def test_helpers_utc(self):
# Here we just test, that we got the correct UTC variant
if not TEST_WITH_OPT_DEPS:
assert tg_dtm.UTC is tg_dtm.DTM_UTC
else:
assert tg_dtm.UTC is not tg_dtm.DTM_UTC
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
def test_localize_pytz(self):
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
import pytz
tzinfo = pytz.timezone("Europe/Berlin")
localized_dt = tg_dtm.localize(dt, tzinfo)
assert localized_dt.hour == dt.hour
assert localized_dt.tzinfo is not None
assert tzinfo.utcoffset(dt) is not None
def test_localize_zoneinfo_naive(self):
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
tzinfo = zoneinfo.ZoneInfo("Europe/Berlin")
localized_dt = tg_dtm.localize(dt, tzinfo)
assert localized_dt.hour == dt.hour
assert localized_dt.tzinfo is not None
assert tzinfo.utcoffset(dt) is not None
def test_localize_zoneinfo_aware(self):
dt = dtm.datetime(2023, 1, 1, 12, 0, 0, tzinfo=dtm.timezone.utc)
tzinfo = zoneinfo.ZoneInfo("Europe/Berlin")
localized_dt = tg_dtm.localize(dt, tzinfo)
assert localized_dt.hour == dt.hour + 1
assert localized_dt.tzinfo is not None
assert tzinfo.utcoffset(dt) is not None
def test_to_float_timestamp_absolute_naive(self):
"""Conversion from timezone-naive datetime to timestamp.
@ -75,20 +96,12 @@ class TestDatetime:
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
assert tg_dtm.to_float_timestamp(datetime) == 1573431976.1
def test_to_float_timestamp_absolute_naive_no_pytz(self, monkeypatch):
"""Conversion from timezone-naive datetime to timestamp.
Naive datetimes should be assumed to be in UTC.
"""
monkeypatch.setattr(tg_dtm, "UTC", tg_dtm.DTM_UTC)
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
assert tg_dtm.to_float_timestamp(datetime) == 1573431976.1
def test_to_float_timestamp_absolute_aware(self, timezone):
"""Conversion from timezone-aware datetime to timestamp"""
# we're parametrizing this with two different UTC offsets to exclude the possibility
# of an xpass when the test is run in a timezone with the same UTC offset
test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
datetime = self.localize(test_datetime, timezone)
datetime = tg_dtm.localize(test_datetime, timezone)
assert (
tg_dtm.to_float_timestamp(datetime)
== 1573431976.1 - timezone.utcoffset(test_datetime).total_seconds()
@ -126,7 +139,7 @@ class TestDatetime:
ref_datetime = dtm.datetime(1970, 1, 1, 12)
utc_offset = timezone.utcoffset(ref_datetime)
ref_t, time_of_day = tg_dtm._datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
aware_time_of_day = self.localize(ref_datetime, timezone).timetz()
aware_time_of_day = tg_dtm.localize(ref_datetime, timezone).timetz()
# first test that naive time is assumed to be utc:
assert tg_dtm.to_float_timestamp(time_of_day, ref_t) == pytest.approx(ref_t)
@ -169,7 +182,7 @@ class TestDatetime:
# we're parametrizing this with two different UTC offsets to exclude the possibility
# of an xpass when the test is run in a timezone with the same UTC offset
test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
datetime = self.localize(test_datetime, timezone)
datetime = tg_dtm.localize(test_datetime, timezone)
assert (
tg_dtm.from_timestamp(1573431976.1 - timezone.utcoffset(test_datetime).total_seconds())
== datetime

View file

@ -21,6 +21,7 @@ import datetime as dtm
import functools
import inspect
import re
import zoneinfo
from collections.abc import Collection, Iterable
from typing import Any, Callable, Optional
@ -40,15 +41,11 @@ from telegram import (
Sticker,
TelegramObject,
)
from telegram._utils.datetime import to_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
from telegram.constants import InputMediaType
from telegram.ext import Defaults, ExtBot
from telegram.request import RequestData
from tests.auxil.envvars import TEST_WITH_OPT_DEPS
if TEST_WITH_OPT_DEPS:
import pytz
FORWARD_REF_PATTERN = re.compile(r"ForwardRef\('(?P<class_name>\w+)'\)")
""" A pattern to find a class name in a ForwardRef typing annotation.
@ -344,10 +341,10 @@ def build_kwargs(
# Some special casing for methods that have "exactly one of the optionals" type args
elif name in ["location", "contact", "venue", "inline_message_id"]:
kws[name] = True
elif name == "until_date":
elif name.endswith("_date"):
if manually_passed_value not in [None, DEFAULT_NONE]:
# Europe/Berlin
kws[name] = pytz.timezone("Europe/Berlin").localize(dtm.datetime(2000, 1, 1, 0))
kws[name] = dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"))
else:
# naive UTC
kws[name] = dtm.datetime(2000, 1, 1, 0)
@ -395,6 +392,15 @@ def make_assertion_for_link_preview_options(
)
_EUROPE_BERLIN_TS = to_timestamp(
dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"))
)
_UTC_TS = to_timestamp(dtm.datetime(2000, 1, 1, 0), tzinfo=zoneinfo.ZoneInfo("UTC"))
_AMERICA_NEW_YORK_TS = to_timestamp(
dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("America/New_York"))
)
async def make_assertion(
url,
request_data: RequestData,
@ -530,14 +536,16 @@ async def make_assertion(
)
# Check datetime conversion
until_date = data.pop("until_date", None)
if until_date:
if manual_value_expected and until_date != 946681200:
pytest.fail("Non-naive until_date should have been interpreted as Europe/Berlin.")
if not any((manually_passed_value, expected_defaults_value)) and until_date != 946684800:
pytest.fail("Naive until_date should have been interpreted as UTC")
if default_value_expected and until_date != 946702800:
pytest.fail("Naive until_date should have been interpreted as America/New_York")
date_keys = [key for key in data if key.endswith("_date")]
for key in date_keys:
date_param = data.pop(key)
if date_param:
if manual_value_expected and date_param != _EUROPE_BERLIN_TS:
pytest.fail(f"Non-naive `{key}` should have been interpreted as Europe/Berlin.")
if not any((manually_passed_value, expected_defaults_value)) and date_param != _UTC_TS:
pytest.fail(f"Naive `{key}` should have been interpreted as UTC")
if default_value_expected and date_param != _AMERICA_NEW_YORK_TS:
pytest.fail(f"Naive `{key}` should have been interpreted as America/New_York")
if method_name in ["get_file", "get_small_file", "get_big_file"]:
# This is here mainly for PassportFile.get_file, which calls .set_credentials on the
@ -596,7 +604,7 @@ async def check_defaults_handling(
defaults_no_custom_defaults = Defaults()
kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters}
kwargs["tzinfo"] = pytz.timezone("America/New_York")
kwargs["tzinfo"] = zoneinfo.ZoneInfo("America/New_York")
kwargs.pop("disable_web_page_preview") # mutually exclusive with link_preview_options
kwargs.pop("quote") # mutually exclusive with do_quote
kwargs["link_preview_options"] = LinkPreviewOptions(

View file

@ -24,6 +24,8 @@ import random
from telegram._utils.strings import TextEncoding
from .envvars import GITHUB_ACTIONS
# Provide some public fallbacks so it's easy for contributors to run tests on their local machine
# These bots are only able to talk in our test chats, so they are quite useless for other
# purposes than testing.
@ -41,10 +43,9 @@ FALLBACKS = (
"NjcmlwdGlvbl9jaGFubmVsX2lkIjogLTEwMDIyMjk2NDkzMDN9XQ=="
)
GITHUB_ACTION = os.getenv("GITHUB_ACTION", None)
BOTS = os.getenv("BOTS", None)
JOB_INDEX = os.getenv("JOB_INDEX", None)
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
if GITHUB_ACTIONS and BOTS is not None and JOB_INDEX is not None:
BOTS = json.loads(base64.b64decode(BOTS).decode(TextEncoding.UTF_8))
JOB_INDEX = int(JOB_INDEX)
@ -60,7 +61,7 @@ class BotInfoProvider:
@staticmethod
def _get_value(key, fallback):
# If we're running as a github action then fetch bots from the repo secrets
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
if GITHUB_ACTIONS and BOTS is not None and JOB_INDEX is not None:
try:
return BOTS[JOB_INDEX][key]
except (IndexError, KeyError):

View file

@ -27,6 +27,9 @@ def env_var_2_bool(env_var: object) -> bool:
return env_var.lower().strip() == "true"
GITHUB_ACTION = os.getenv("GITHUB_ACTION", "")
TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "true"))
RUN_TEST_OFFICIAL = env_var_2_bool(os.getenv("TEST_OFFICIAL"))
GITHUB_ACTIONS: bool = env_var_2_bool(os.getenv("GITHUB_ACTIONS", "false"))
TEST_WITH_OPT_DEPS: bool = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) or (
# on local setups, we usually want to test with optional dependencies
not GITHUB_ACTIONS
)
RUN_TEST_OFFICIAL: bool = env_var_2_bool(os.getenv("TEST_OFFICIAL"))

View file

@ -17,9 +17,9 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import asyncio
import datetime as dtm
import logging
import sys
import zoneinfo
from pathlib import Path
from uuid import uuid4
@ -40,11 +40,10 @@ from telegram.ext import Defaults
from tests.auxil.build_messages import DATE
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_ACTION, RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS
from tests.auxil.envvars import GITHUB_ACTIONS, RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS
from tests.auxil.files import data_file
from tests.auxil.networking import NonchalantHttpxRequest
from tests.auxil.pytest_classes import PytestBot, make_bot
from tests.auxil.timezones import BasicTimezone
if TEST_WITH_OPT_DEPS:
import pytz
@ -97,7 +96,7 @@ def pytest_collection_modifyitems(items: list[pytest.Item]):
parent.add_marker(pytest.mark.no_req)
if GITHUB_ACTION and JOB_INDEX == 0:
if GITHUB_ACTIONS and JOB_INDEX == 0:
# let's not slow down the tests too much with these additional checks
# that's why we run them only in GitHub actions and only on *one* of the several test
# matrix entries
@ -308,12 +307,20 @@ def false_update(request):
return Update(update_id=1, **request.param)
@pytest.fixture(
scope="session",
params=[pytz.timezone, zoneinfo.ZoneInfo] if TEST_WITH_OPT_DEPS else [zoneinfo.ZoneInfo],
)
def _tz_implementation(request): # noqa: PT005
# This fixture is used to parametrize the timezone fixture
# This is similar to what @pyttest.mark.parametrize does but for fixtures
# However, this is needed only internally for the `tzinfo` fixture, so we keep it private
return request.param
@pytest.fixture(scope="session", params=["Europe/Berlin", "Asia/Singapore", "UTC"])
def tzinfo(request):
if TEST_WITH_OPT_DEPS:
return pytz.timezone(request.param)
hours_offset = {"Europe/Berlin": 2, "Asia/Singapore": 8, "UTC": 0}[request.param]
return BasicTimezone(offset=dtm.timedelta(hours=hours_offset), name=request.param)
def tzinfo(request, _tz_implementation):
return _tz_implementation(request.param)
@pytest.fixture(scope="session")

View file

@ -38,10 +38,17 @@ class TestDefaults:
def test_utc(self):
defaults = Defaults()
if not TEST_WITH_OPT_DEPS:
assert defaults.tzinfo is dtm.timezone.utc
else:
assert defaults.tzinfo is not dtm.timezone.utc
assert defaults.tzinfo is dtm.timezone.utc
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
def test_pytz_deprecation(self, recwarn):
import pytz
with pytest.warns(PTBDeprecationWarning, match="pytz") as record:
Defaults(tzinfo=pytz.timezone("Europe/Berlin"))
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
def test_data_assignment(self):
defaults = Defaults()

View file

@ -18,6 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
import asyncio
import calendar
import contextlib
import datetime as dtm
import logging
import platform
@ -26,7 +27,7 @@ import time
import pytest
from telegram.ext import ApplicationBuilder, CallbackContext, ContextTypes, Defaults, Job, JobQueue
from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS
from tests.auxil.envvars import GITHUB_ACTIONS, TEST_WITH_OPT_DEPS
from tests.auxil.pytest_classes import make_bot
from tests.auxil.slots import mro_slots
@ -68,7 +69,7 @@ class TestNoJobQueue:
not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed"
)
@pytest.mark.skipif(
bool(GITHUB_ACTION and platform.system() in ["Windows", "Darwin"]),
GITHUB_ACTIONS and platform.system() in ["Windows", "Darwin"],
reason="On Windows & MacOS precise timings are not accurate.",
)
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect
@ -77,6 +78,13 @@ class TestJobQueue:
job_time = 0
received_error = None
@staticmethod
def normalize(datetime: dtm.datetime, timezone: dtm.tzinfo) -> dtm.datetime:
with contextlib.suppress(AttributeError):
return timezone.normalize(datetime)
return datetime
async def test_repr(self, app):
jq = JobQueue()
jq.set_application(app)
@ -102,7 +110,7 @@ class TestJobQueue:
# Unfortunately, we can't really test the executor setting explicitly without relying
# on protected attributes. However, this should be tested enough implicitly via all the
# other tests in here
assert job_queue.scheduler_configuration["timezone"] is UTC
assert job_queue.scheduler_configuration["timezone"] is dtm.timezone.utc
tz_app = ApplicationBuilder().defaults(Defaults(tzinfo=timezone)).token(bot.token).build()
assert tz_app.job_queue.scheduler_configuration["timezone"] is timezone
@ -356,6 +364,17 @@ class TestJobQueue:
scheduled_time = job_queue.jobs()[0].next_t.timestamp()
assert scheduled_time == pytest.approx(expected_time)
async def test_time_unit_dt_aware_time(self, job_queue, timezone):
# Testing running at a specific tz-aware time today
delta, now = 0.5, dtm.datetime.now(timezone)
expected_time = now + dtm.timedelta(seconds=delta)
when = expected_time.timetz()
expected_time = expected_time.timestamp()
job_queue.run_once(self.job_datetime_tests, when)
await asyncio.sleep(0.6)
assert self.job_time == pytest.approx(expected_time)
async def test_run_daily(self, job_queue):
delta, now = 1, dtm.datetime.now(UTC)
time_of_day = (now + dtm.timedelta(seconds=delta)).time()
@ -397,7 +416,7 @@ class TestJobQueue:
if day > next_months_days:
expected_reschedule_time += dtm.timedelta(next_months_days)
expected_reschedule_time = timezone.normalize(expected_reschedule_time)
expected_reschedule_time = self.normalize(expected_reschedule_time, timezone)
# Adjust the hour for the special case that between now and next month a DST switch happens
expected_reschedule_time += dtm.timedelta(
hours=time_of_day.hour - expected_reschedule_time.hour
@ -419,7 +438,7 @@ class TestJobQueue:
calendar.monthrange(now.year, now.month)[1]
) - dtm.timedelta(days=now.day)
# Adjust the hour for the special case that between now & end of month a DST switch happens
expected_reschedule_time = timezone.normalize(expected_reschedule_time)
expected_reschedule_time = self.normalize(expected_reschedule_time, timezone)
expected_reschedule_time += dtm.timedelta(
hours=time_of_day.hour - expected_reschedule_time.hour
)

View file

@ -35,7 +35,7 @@ from telegram.constants import ParseMode
from telegram.error import RetryAfter
from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot
from telegram.request import BaseRequest, RequestData
from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS
from tests.auxil.envvars import GITHUB_ACTIONS, TEST_WITH_OPT_DEPS
@pytest.mark.skipif(
@ -142,7 +142,7 @@ class TestBaseRateLimiter:
not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed"
)
@pytest.mark.skipif(
bool(GITHUB_ACTION and platform.system() == "Darwin"),
GITHUB_ACTIONS and platform.system() == "Darwin",
reason="The timings are apparently rather inaccurate on MacOS.",
)
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect

View file

@ -79,7 +79,7 @@ from telegram import (
User,
WebAppInfo,
)
from telegram._utils.datetime import UTC, from_timestamp, to_timestamp
from telegram._utils.datetime import UTC, from_timestamp, localize, to_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.strings import to_camel_case
from telegram.constants import (
@ -97,7 +97,7 @@ from telegram.request import BaseRequest, HTTPXRequest, RequestData
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
from tests.auxil.bot_method_checks import check_defaults_handling
from tests.auxil.ci_bots import FALLBACKS
from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS
from tests.auxil.envvars import GITHUB_ACTIONS
from tests.auxil.files import data_file
from tests.auxil.networking import OfflineRequest, expect_bad_request
from tests.auxil.pytest_classes import PytestBot, PytestExtBot, make_bot
@ -154,7 +154,7 @@ def inline_results():
BASE_GAME_SCORE = 60 # Base game score for game tests
xfail = pytest.mark.xfail(
bool(GITHUB_ACTION), # This condition is only relevant for github actions game tests.
GITHUB_ACTIONS, # This condition is only relevant for github actions game tests.
reason=(
"Can fail due to race conditions when multiple test suites "
"with the same bot token are run at the same time"
@ -3467,7 +3467,6 @@ class TestBotWithRequest:
)
assert revoked_link.is_revoked
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="This test's implementation requires pytz")
@pytest.mark.parametrize("datetime", argvalues=[True, False], ids=["datetime", "integer"])
async def test_advanced_chat_invite_links(self, bot, channel_id, datetime):
# we are testing this all in one function in order to save api calls
@ -3475,7 +3474,7 @@ class TestBotWithRequest:
add_seconds = dtm.timedelta(0, 70)
time_in_future = timestamp + add_seconds
expire_time = time_in_future if datetime else to_timestamp(time_in_future)
aware_time_in_future = UTC.localize(time_in_future)
aware_time_in_future = localize(time_in_future, UTC)
invite_link = await bot.create_chat_invite_link(
channel_id, expire_date=expire_time, member_limit=10
@ -3488,7 +3487,7 @@ class TestBotWithRequest:
add_seconds = dtm.timedelta(0, 80)
time_in_future = timestamp + add_seconds
expire_time = time_in_future if datetime else to_timestamp(time_in_future)
aware_time_in_future = UTC.localize(time_in_future)
aware_time_in_future = localize(time_in_future, UTC)
edited_invite_link = await bot.edit_chat_invite_link(
channel_id,

View file

@ -58,7 +58,7 @@ class TestConstantsWithoutRequest:
not key.startswith("_")
# exclude imported stuff
and getattr(member, "__module__", "telegram.constants") == "telegram.constants"
and key not in ("sys", "dtm")
and key not in ("sys", "dtm", "UTC")
)
}
actual = set(constants.__all__)