From 8c02b05550c5b408d9baef51efc700a8a3706499 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:32:39 +0000 Subject: [PATCH 01/15] Update apscheduler requirement from ~=3.10.4 to >=3.10.4,<3.12.0 Updates the requirements on [apscheduler](https://github.com/agronholm/apscheduler) to permit the latest version. - [Release notes](https://github.com/agronholm/apscheduler/releases) - [Changelog](https://github.com/agronholm/apscheduler/blob/3.11.0/docs/versionhistory.rst) - [Commits](https://github.com/agronholm/apscheduler/compare/3.10.4...3.11.0) --- updated-dependencies: - dependency-name: apscheduler dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 58752295610..1713896d459 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ http2 = [ ] job-queue = [ # APS doesn't have a strict stability policy. Let's be cautious for now. - "APScheduler~=3.10.4", + "APScheduler>=3.10.4,<3.12.0", # pytz is required by APS and just needs the lower bound due to #2120 "pytz>=2018.6", ] From fcccd3299107a90eca26d3b97848bea3789bbb0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:20:58 +0000 Subject: [PATCH 02/15] Update apscheduler requirement from ~=3.10.4 to >=3.10.4,<3.12.0 Updates the requirements on [apscheduler](https://github.com/agronholm/apscheduler) to permit the latest version. - [Release notes](https://github.com/agronholm/apscheduler/releases) - [Changelog](https://github.com/agronholm/apscheduler/blob/3.11.0/docs/versionhistory.rst) - [Commits](https://github.com/agronholm/apscheduler/compare/3.10.4...3.11.0) --- updated-dependencies: - dependency-name: apscheduler dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6fba965299d..7e57e14c317 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ http2 = [ ] job-queue = [ # APS doesn't have a strict stability policy. Let's be cautious for now. - "APScheduler~=3.10.4", + "APScheduler>=3.10.4,<3.12.0", # pytz is required by APS and just needs the lower bound due to #2120 "pytz>=2018.6", ] From 84c93bb91a74ae5ee1b0762f9e46a56143591a80 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:16:05 +0100 Subject: [PATCH 03/15] Try getting independent of pytz --- README.rst | 2 +- pyproject.toml | 2 -- telegram/_utils/datetime.py | 31 +++++++++++++++++-------------- telegram/ext/_defaults.py | 4 +--- telegram/ext/_jobqueue.py | 10 +++++----- tests/auxil/bot_method_checks.py | 9 +++++---- tests/conftest.py | 8 +++----- tests/test_bot.py | 9 ++++----- 8 files changed, 36 insertions(+), 39 deletions(-) diff --git a/README.rst b/README.rst index 3721a834fd0..f93c1d8c93e 100644 --- a/README.rst +++ b/README.rst @@ -158,7 +158,7 @@ PTB can be installed with optional dependencies: * ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1,<1.3 `_. Use this, if you want to use ``telegram.ext.AIORateLimiter``. * ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 `_ 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 `_ library. Use this, if you want to use `arbitrary callback_data `_. -* ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 `_ library and enforces `pytz>=2018.6 `_, 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 `_ 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]"``. diff --git a/pyproject.toml b/pyproject.toml index 1713896d459..3fb17f00e5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,8 +77,6 @@ http2 = [ job-queue = [ # APS doesn't have a strict stability policy. Let's be cautious for now. "APScheduler>=3.10.4,<3.12.0", - # pytz is required by APS and just needs the lower bound due to #2120 - "pytz>=2018.6", ] passport = [ "cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1", diff --git a/telegram/_utils/datetime.py b/telegram/_utils/datetime.py index 40d931efffe..abb7623cb67 100644 --- a/telegram/_utils/datetime.py +++ b/telegram/_utils/datetime.py @@ -27,6 +27,7 @@ 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,24 @@ 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, 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) -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] + return datetime.astimezone(tzinfo) def to_float_timestamp( @@ -87,7 +90,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``. @@ -132,7 +135,7 @@ 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) + aware_datetime = localize(aware_datetime, tzinfo) # if the time of day has passed today, use tomorrow if reference_time > aware_datetime.timetz(): @@ -140,7 +143,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") diff --git a/telegram/ext/_defaults.py b/telegram/ext/_defaults.py index 03191462252..a8ef66f31fa 100644 --- a/telegram/ext/_defaults.py +++ b/telegram/ext/_defaults.py @@ -57,9 +57,7 @@ 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. block (:obj:`bool`, optional): Default setting for the :paramref:`BaseHandler.block` parameter diff --git a/telegram/ext/_jobqueue.py b/telegram/ext/_jobqueue.py index b73d0545e22..ce5c03587db 100644 --- a/telegram/ext/_jobqueue.py +++ b/telegram/ext/_jobqueue.py @@ -23,7 +23,6 @@ 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 @@ 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 @@ def scheduler_configuration(self) -> JSONDict: dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary. """ - timezone: object = pytz.utc + timezone: datetime.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,8 @@ def _parse_time_input( datetime.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 <= datetime.datetime.now(pytz.utc): + date_time = localize(date_time, self.scheduler.timezone) + if shift_day and date_time <= datetime.datetime.now(UTC): date_time += datetime.timedelta(days=1) return date_time return time diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index a498693cea7..5d91f89983b 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -21,6 +21,7 @@ import functools import inspect import re +import zoneinfo from collections.abc import Collection, Iterable from typing import Any, Callable, Optional @@ -46,7 +47,7 @@ from tests.auxil.envvars import TEST_WITH_OPT_DEPS if TEST_WITH_OPT_DEPS: - import pytz + pass FORWARD_REF_PATTERN = re.compile(r"ForwardRef\('(?P\w+)'\)") @@ -336,8 +337,8 @@ def build_kwargs( elif name == "until_date": if manually_passed_value not in [None, DEFAULT_NONE]: # Europe/Berlin - kws[name] = pytz.timezone("Europe/Berlin").localize( - datetime.datetime(2000, 1, 1, 0) + kws[name] = datetime.datetime( + 2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin") ) else: # naive UTC @@ -587,7 +588,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( diff --git a/tests/conftest.py b/tests/conftest.py index 70a19009624..610d998db95 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 import logging import sys +import zoneinfo from pathlib import Path from uuid import uuid4 @@ -44,7 +44,6 @@ 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 @@ -311,9 +310,8 @@ def false_update(request): @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=datetime.timedelta(hours=hours_offset), name=request.param) + yield pytz.timezone(request.param) + yield zoneinfo.ZoneInfo(request.param) @pytest.fixture(scope="session") diff --git a/tests/test_bot.py b/tests/test_bot.py index 7977efec36c..ec17ca89b26 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -79,7 +79,7 @@ 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.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_ACTION 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 @@ -3467,7 +3467,6 @@ async def test_create_chat_invite_link_basics( ) 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 @@ async def test_advanced_chat_invite_links(self, bot, channel_id, datetime): 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 @@ async def test_advanced_chat_invite_links(self, bot, channel_id, datetime): 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, From 5cc7da8430ff1177469de506fd8c1bb3ea720455 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:24:50 +0100 Subject: [PATCH 04/15] Get existing tests to run --- telegram/_utils/datetime.py | 2 ++ tests/_utils/test_datetime.py | 59 +++++++++++++++++++------------- tests/auxil/bot_method_checks.py | 58 +++++++++++++++---------------- tests/conftest.py | 17 ++++++--- tests/ext/test_defaults.py | 6 +--- tests/ext/test_jobqueue.py | 3 ++ tests/test_constants.py | 2 +- 7 files changed, 85 insertions(+), 62 deletions(-) diff --git a/telegram/_utils/datetime.py b/telegram/_utils/datetime.py index abb7623cb67..2baf089d7e0 100644 --- a/telegram/_utils/datetime.py +++ b/telegram/_utils/datetime.py @@ -52,6 +52,8 @@ def localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime: if isinstance(tzinfo, pytz.BaseTzInfo): return tzinfo.localize(datetime) + if datetime.tzinfo is None: + return datetime.replace(tzinfo=tzinfo) return datetime.astimezone(tzinfo) diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py index 7ef28468839..58f0b18e14d 100644 --- a/tests/_utils/test_datetime.py +++ b/tests/_utils/test_datetime.py @@ -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 @@ class TestDatetime: - @staticmethod - def localize(dt, tzinfo): - if TEST_WITH_OPT_DEPS: - return tzinfo.localize(dt) - return dt.replace(tzinfo=tzinfo) - - 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 + 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) + + @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 @@ def test_to_float_timestamp_absolute_naive(self): 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 @@ def test_to_float_timestamp_time_of_day_timezone(self, timezone): 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 @@ def test_from_timestamp_aware(self, timezone): # 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 diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 58bffef6f3a..317042d2520 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -636,35 +636,35 @@ async def check_defaults_handling( request.post = assertion_callback assert await method(**kwargs) in expected_return_values - # 2: test that we get the manually passed non-None value - kwargs = build_kwargs( - shortcut_signature, kwargs_need_default, manually_passed_value="non-None-value" - ) - assertion_callback = functools.partial( - make_assertion, - manually_passed_value="non-None-value", - kwargs_need_default=kwargs_need_default, - method_name=method.__name__, - return_value=return_value, - expected_defaults_value=expected_defaults_value, - ) - request.post = assertion_callback - assert await method(**kwargs) in expected_return_values - - # 3: test that we get the manually passed None value - kwargs = build_kwargs( - shortcut_signature, kwargs_need_default, manually_passed_value=None - ) - assertion_callback = functools.partial( - make_assertion, - manually_passed_value=None, - kwargs_need_default=kwargs_need_default, - method_name=method.__name__, - return_value=return_value, - expected_defaults_value=expected_defaults_value, - ) - request.post = assertion_callback - assert await method(**kwargs) in expected_return_values + # # 2: test that we get the manually passed non-None value + # kwargs = build_kwargs( + # shortcut_signature, kwargs_need_default, manually_passed_value="non-None-value" + # ) + # assertion_callback = functools.partial( + # make_assertion, + # manually_passed_value="non-None-value", + # kwargs_need_default=kwargs_need_default, + # method_name=method.__name__, + # return_value=return_value, + # expected_defaults_value=expected_defaults_value, + # ) + # request.post = assertion_callback + # assert await method(**kwargs) in expected_return_values + # + # # 3: test that we get the manually passed None value + # kwargs = build_kwargs( + # shortcut_signature, kwargs_need_default, manually_passed_value=None + # ) + # assertion_callback = functools.partial( + # make_assertion, + # manually_passed_value=None, + # kwargs_need_default=kwargs_need_default, + # method_name=method.__name__, + # return_value=return_value, + # expected_defaults_value=expected_defaults_value, + # ) + # request.post = assertion_callback + # assert await method(**kwargs) in expected_return_values except Exception as exc: raise exc finally: diff --git a/tests/conftest.py b/tests/conftest.py index 610d998db95..a1b45e49b47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -307,11 +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: - yield pytz.timezone(request.param) - yield zoneinfo.ZoneInfo(request.param) +def tzinfo(request, _tz_implementation): + return _tz_implementation(request.param) @pytest.fixture(scope="session") diff --git a/tests/ext/test_defaults.py b/tests/ext/test_defaults.py index abc00aa61ad..5044e14332d 100644 --- a/tests/ext/test_defaults.py +++ b/tests/ext/test_defaults.py @@ -25,7 +25,6 @@ from telegram import LinkPreviewOptions, User from telegram.ext import Defaults from telegram.warnings import PTBDeprecationWarning -from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.slots import mro_slots @@ -38,10 +37,7 @@ def test_slot_behaviour(self): 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 def test_data_assignment(self): defaults = Defaults() diff --git a/tests/ext/test_jobqueue.py b/tests/ext/test_jobqueue.py index 5ca908d4a02..5aa57d6118f 100644 --- a/tests/ext/test_jobqueue.py +++ b/tests/ext/test_jobqueue.py @@ -102,6 +102,9 @@ def test_scheduler_configuration(self, job_queue, timezone, bot): # 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 + tz = job_queue.scheduler_configuration["timezone"] + print(tz, repr(tz), type(tz)) + print(UTC, repr(UTC), type(UTC)) assert job_queue.scheduler_configuration["timezone"] is UTC tz_app = ApplicationBuilder().defaults(Defaults(tzinfo=timezone)).token(bot.token).build() diff --git a/tests/test_constants.py b/tests/test_constants.py index f85e09624a9..e43634d9c6b 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -58,7 +58,7 @@ def test__all__(self): 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__) From 1945ce7c73d2f6ecbf92fac0b90974a23aac4968 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:28:53 +0100 Subject: [PATCH 05/15] Extend defaults testing to other _date parameters --- tests/auxil/bot_method_checks.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 317042d2520..e3d4d2a5375 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -345,7 +345,7 @@ 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] = dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")) @@ -531,14 +531,19 @@ def check_input_media(m: dict): ) # 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 != 946681200: + 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 != 946684800 + ): + pytest.fail(f"Naive `{key}` should have been interpreted as UTC") + if default_value_expected and date_param != 946702800: + 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 From e846251242c5653145acc1cdcf00b14f720f33a9 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:51:24 +0100 Subject: [PATCH 06/15] Mark pytz support as deprecated --- .github/workflows/unit_tests.yml | 3 ++- telegram/ext/_defaults.py | 18 ++++++++++++++++++ tests/ext/test_defaults.py | 11 +++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 76eff5ec8d6..1e16110e343 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -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. diff --git a/telegram/ext/_defaults.py b/telegram/ext/_defaults.py index 2f7c22f74cc..eb33e5e4419 100644 --- a/telegram/ext/_defaults.py +++ b/telegram/ext/_defaults.py @@ -59,6 +59,11 @@ class Defaults: 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`. 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 @@ -146,6 +151,19 @@ def __init__( 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." diff --git a/tests/ext/test_defaults.py b/tests/ext/test_defaults.py index 5044e14332d..621e6bd74f2 100644 --- a/tests/ext/test_defaults.py +++ b/tests/ext/test_defaults.py @@ -25,6 +25,7 @@ from telegram import LinkPreviewOptions, User from telegram.ext import Defaults from telegram.warnings import PTBDeprecationWarning +from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.slots import mro_slots @@ -39,6 +40,16 @@ def test_utc(self): defaults = Defaults() 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() From 37dfdf8b8af458b91d1be1a235428612cda3f764 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:57:40 +0100 Subject: [PATCH 07/15] try fixing workflows --- tests/auxil/envvars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/auxil/envvars.py b/tests/auxil/envvars.py index 1b360e5d551..d9812a43cd1 100644 --- a/tests/auxil/envvars.py +++ b/tests/auxil/envvars.py @@ -28,5 +28,5 @@ def env_var_2_bool(env_var: object) -> bool: GITHUB_ACTION = os.getenv("GITHUB_ACTION", "") -TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "true")) +TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) RUN_TEST_OFFICIAL = env_var_2_bool(os.getenv("TEST_OFFICIAL")) From ad4d460591ec67d8d684897546ca067b1087b95a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:17:51 +0100 Subject: [PATCH 08/15] try fixing jobqueue tests --- tests/ext/test_jobqueue.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/ext/test_jobqueue.py b/tests/ext/test_jobqueue.py index 5aa57d6118f..aa737442ee5 100644 --- a/tests/ext/test_jobqueue.py +++ b/tests/ext/test_jobqueue.py @@ -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 @@ -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) @@ -400,7 +408,7 @@ async def test_run_monthly(self, job_queue, timezone): 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 @@ -422,7 +430,7 @@ async def test_run_monthly_non_strict_day(self, job_queue, timezone): 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 ) From 5008606c4c6b35db31c91436443dd10468157a05 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:28:44 +0100 Subject: [PATCH 09/15] try again --- tests/ext/test_jobqueue.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/ext/test_jobqueue.py b/tests/ext/test_jobqueue.py index aa737442ee5..d41a0908418 100644 --- a/tests/ext/test_jobqueue.py +++ b/tests/ext/test_jobqueue.py @@ -110,10 +110,7 @@ def test_scheduler_configuration(self, job_queue, timezone, bot): # 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 - tz = job_queue.scheduler_configuration["timezone"] - print(tz, repr(tz), type(tz)) - print(UTC, repr(UTC), type(UTC)) - 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 From 1f8124f3cb2bd9f16d926da150df6f1048822a6f Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:34:48 +0100 Subject: [PATCH 10/15] add tzdata to unit test requirements --- requirements-unit-tests.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt index 654bc9e9fdb..a9c4ba3c2c9 100644 --- a/requirements-unit-tests.txt +++ b/requirements-unit-tests.txt @@ -16,4 +16,8 @@ pytest-xdist==3.6.1 flaky>=3.8.1 # used in test_official for parsing tg docs -beautifulsoup4 \ No newline at end of file +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 \ No newline at end of file From 9cabf0e03b3569c956d00275e0c0e90cb14dadcc Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:14:24 +0100 Subject: [PATCH 11/15] Review --- telegram/_utils/datetime.py | 8 ++++ telegram/ext/_jobqueue.py | 2 + tests/auxil/bot_method_checks.py | 82 ++++++++++++++++---------------- tests/ext/test_jobqueue.py | 28 +++++++---- 4 files changed, 71 insertions(+), 49 deletions(-) diff --git a/telegram/_utils/datetime.py b/telegram/_utils/datetime.py index 2baf089d7e0..1616e88bc83 100644 --- a/telegram/_utils/datetime.py +++ b/telegram/_utils/datetime.py @@ -126,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): @@ -137,6 +143,8 @@ def to_float_timestamp( aware_datetime = dtm.datetime.combine(reference_date, time_object) if aware_datetime.tzinfo is None: + # 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 diff --git a/telegram/ext/_jobqueue.py b/telegram/ext/_jobqueue.py index 7408daee29d..fffeff8b591 100644 --- a/telegram/ext/_jobqueue.py +++ b/telegram/ext/_jobqueue.py @@ -197,6 +197,8 @@ def _parse_time_input( dtm.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time ) if date_time.tzinfo is None: + # 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) diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index e3d4d2a5375..ee28c27b5f2 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -41,15 +41,11 @@ 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: - pass - FORWARD_REF_PATTERN = re.compile(r"ForwardRef\('(?P\w+)'\)") """ A pattern to find a class name in a ForwardRef typing annotation. @@ -396,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, @@ -535,14 +540,11 @@ def check_input_media(m: dict): for key in date_keys: date_param = data.pop(key) if date_param: - if manual_value_expected and date_param != 946681200: + 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 != 946684800 - ): + 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 != 946702800: + 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"]: @@ -641,35 +643,35 @@ async def check_defaults_handling( request.post = assertion_callback assert await method(**kwargs) in expected_return_values - # # 2: test that we get the manually passed non-None value - # kwargs = build_kwargs( - # shortcut_signature, kwargs_need_default, manually_passed_value="non-None-value" - # ) - # assertion_callback = functools.partial( - # make_assertion, - # manually_passed_value="non-None-value", - # kwargs_need_default=kwargs_need_default, - # method_name=method.__name__, - # return_value=return_value, - # expected_defaults_value=expected_defaults_value, - # ) - # request.post = assertion_callback - # assert await method(**kwargs) in expected_return_values - # - # # 3: test that we get the manually passed None value - # kwargs = build_kwargs( - # shortcut_signature, kwargs_need_default, manually_passed_value=None - # ) - # assertion_callback = functools.partial( - # make_assertion, - # manually_passed_value=None, - # kwargs_need_default=kwargs_need_default, - # method_name=method.__name__, - # return_value=return_value, - # expected_defaults_value=expected_defaults_value, - # ) - # request.post = assertion_callback - # assert await method(**kwargs) in expected_return_values + # 2: test that we get the manually passed non-None value + kwargs = build_kwargs( + shortcut_signature, kwargs_need_default, manually_passed_value="non-None-value" + ) + assertion_callback = functools.partial( + make_assertion, + manually_passed_value="non-None-value", + kwargs_need_default=kwargs_need_default, + method_name=method.__name__, + return_value=return_value, + expected_defaults_value=expected_defaults_value, + ) + request.post = assertion_callback + assert await method(**kwargs) in expected_return_values + + # 3: test that we get the manually passed None value + kwargs = build_kwargs( + shortcut_signature, kwargs_need_default, manually_passed_value=None + ) + assertion_callback = functools.partial( + make_assertion, + manually_passed_value=None, + kwargs_need_default=kwargs_need_default, + method_name=method.__name__, + return_value=return_value, + expected_defaults_value=expected_defaults_value, + ) + request.post = assertion_callback + assert await method(**kwargs) in expected_return_values except Exception as exc: raise exc finally: diff --git a/tests/ext/test_jobqueue.py b/tests/ext/test_jobqueue.py index d41a0908418..cce36472663 100644 --- a/tests/ext/test_jobqueue.py +++ b/tests/ext/test_jobqueue.py @@ -21,13 +21,12 @@ import contextlib import datetime as dtm import logging -import platform 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 TEST_WITH_OPT_DEPS from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots @@ -65,13 +64,13 @@ def test_init_job(self): Job(None) -@pytest.mark.skipif( - 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"]), - reason="On Windows & MacOS precise timings are not accurate.", -) +# @pytest.mark.skipif( +# 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"]), +# reason="On Windows & MacOS precise timings are not accurate.", +# ) @pytest.mark.flaky(10, 1) # Timings aren't quite perfect class TestJobQueue: result = 0 @@ -364,6 +363,17 @@ async def test_time_unit_dt_time_tomorrow(self, job_queue): 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() From 92851a4c7b58cefdb98d6d1a32033d84f82c9f65 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:33:12 +0100 Subject: [PATCH 12/15] Revert debug changes and improve TEST_WITH_OPT_DEPS handling --- tests/auxil/envvars.py | 9 ++++++--- tests/ext/test_jobqueue.py | 17 +++++++++-------- tests/ext/test_ratelimiter.py | 2 +- tests/test_bot.py | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/auxil/envvars.py b/tests/auxil/envvars.py index d9812a43cd1..07cc2ed296a 100644 --- a/tests/auxil/envvars.py +++ b/tests/auxil/envvars.py @@ -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", "false")) -RUN_TEST_OFFICIAL = env_var_2_bool(os.getenv("TEST_OFFICIAL")) +GITHUB_ACTION: bool = env_var_2_bool(os.getenv("GITHUB_ACTION", "false")) +TEST_WITH_OPT_DEPS: bool = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "")) or ( + # on local setups, we usually want to test with optional dependencies + not GITHUB_ACTION +) +RUN_TEST_OFFICIAL: bool = env_var_2_bool(os.getenv("TEST_OFFICIAL")) diff --git a/tests/ext/test_jobqueue.py b/tests/ext/test_jobqueue.py index cce36472663..33dafb16b50 100644 --- a/tests/ext/test_jobqueue.py +++ b/tests/ext/test_jobqueue.py @@ -21,12 +21,13 @@ import contextlib import datetime as dtm import logging +import platform import time import pytest from telegram.ext import ApplicationBuilder, CallbackContext, ContextTypes, Defaults, Job, JobQueue -from tests.auxil.envvars import TEST_WITH_OPT_DEPS +from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots @@ -64,13 +65,13 @@ def test_init_job(self): Job(None) -# @pytest.mark.skipif( -# 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"]), -# reason="On Windows & MacOS precise timings are not accurate.", -# ) +@pytest.mark.skipif( + not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed" +) +@pytest.mark.skipif( + GITHUB_ACTION 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 class TestJobQueue: result = 0 diff --git a/tests/ext/test_ratelimiter.py b/tests/ext/test_ratelimiter.py index 8af1e541118..56975b1cbcb 100644 --- a/tests/ext/test_ratelimiter.py +++ b/tests/ext/test_ratelimiter.py @@ -142,7 +142,7 @@ async def do_request(self, *args, **kwargs): 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_ACTION and platform.system() == "Darwin", reason="The timings are apparently rather inaccurate on MacOS.", ) @pytest.mark.flaky(10, 1) # Timings aren't quite perfect diff --git a/tests/test_bot.py b/tests/test_bot.py index ec17ca89b26..07d72fcf5db 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -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_ACTION, # 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" From ebb71ababba2bfe59203defeea3bf4d56d03c291 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:50:18 +0100 Subject: [PATCH 13/15] try adding some debug things --- tests/auxil/envvars.py | 2 +- tests/conftest.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/auxil/envvars.py b/tests/auxil/envvars.py index 07cc2ed296a..84e3e8778aa 100644 --- a/tests/auxil/envvars.py +++ b/tests/auxil/envvars.py @@ -28,7 +28,7 @@ def env_var_2_bool(env_var: object) -> bool: GITHUB_ACTION: bool = env_var_2_bool(os.getenv("GITHUB_ACTION", "false")) -TEST_WITH_OPT_DEPS: bool = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "")) or ( +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_ACTION ) diff --git a/tests/conftest.py b/tests/conftest.py index a1b45e49b47..f1583880582 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -40,12 +41,21 @@ 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_ACTION, + 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 if TEST_WITH_OPT_DEPS: + assert GITHUB_ACTION is True + assert (env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) or not GITHUB_ACTION) is True + assert os.getenv("TEST_WITH_OPT_DEPS", "false") == "true" + assert env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) is True import pytz From a98bc6541214f88576f9d1db4d532f7446cdd4b2 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:54:11 +0100 Subject: [PATCH 14/15] Try fixing github action env var --- tests/auxil/ci_bots.py | 7 ++++--- tests/auxil/envvars.py | 4 ++-- tests/conftest.py | 8 ++++---- tests/ext/test_jobqueue.py | 4 ++-- tests/ext/test_ratelimiter.py | 4 ++-- tests/test_bot.py | 4 ++-- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/auxil/ci_bots.py b/tests/auxil/ci_bots.py index 069a65ccec9..9aa586832aa 100644 --- a/tests/auxil/ci_bots.py +++ b/tests/auxil/ci_bots.py @@ -24,6 +24,8 @@ 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 @@ "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 @@ def __init__(self): @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): diff --git a/tests/auxil/envvars.py b/tests/auxil/envvars.py index 84e3e8778aa..a897720e486 100644 --- a/tests/auxil/envvars.py +++ b/tests/auxil/envvars.py @@ -27,9 +27,9 @@ def env_var_2_bool(env_var: object) -> bool: return env_var.lower().strip() == "true" -GITHUB_ACTION: bool = env_var_2_bool(os.getenv("GITHUB_ACTION", "false")) +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_ACTION + not GITHUB_ACTIONS ) RUN_TEST_OFFICIAL: bool = env_var_2_bool(os.getenv("TEST_OFFICIAL")) diff --git a/tests/conftest.py b/tests/conftest.py index f1583880582..a20a584fa7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ 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, + GITHUB_ACTIONS, RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS, env_var_2_bool, @@ -52,8 +52,8 @@ from tests.auxil.pytest_classes import PytestBot, make_bot if TEST_WITH_OPT_DEPS: - assert GITHUB_ACTION is True - assert (env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) or not GITHUB_ACTION) is True + assert GITHUB_ACTIONS is True + assert (env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) or not GITHUB_ACTIONS) is True assert os.getenv("TEST_WITH_OPT_DEPS", "false") == "true" assert env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) is True import pytz @@ -106,7 +106,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 diff --git a/tests/ext/test_jobqueue.py b/tests/ext/test_jobqueue.py index 33dafb16b50..87340ad8c0b 100644 --- a/tests/ext/test_jobqueue.py +++ b/tests/ext/test_jobqueue.py @@ -27,7 +27,7 @@ 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 @@ -69,7 +69,7 @@ def test_init_job(self): not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed" ) @pytest.mark.skipif( - 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 diff --git a/tests/ext/test_ratelimiter.py b/tests/ext/test_ratelimiter.py index 56975b1cbcb..19e9962773e 100644 --- a/tests/ext/test_ratelimiter.py +++ b/tests/ext/test_ratelimiter.py @@ -35,7 +35,7 @@ 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 @@ async def do_request(self, *args, **kwargs): not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed" ) @pytest.mark.skipif( - 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 diff --git a/tests/test_bot.py b/tests/test_bot.py index 07d72fcf5db..519f5aab7ab 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -97,7 +97,7 @@ 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 +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( - 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" From f992b7e6dd787349bfa43a67716043f358775f1f Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:56:44 +0100 Subject: [PATCH 15/15] Remove debug assertions --- tests/conftest.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a20a584fa7a..38326a01ddc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,6 @@ # 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 @@ -41,21 +40,12 @@ 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_ACTIONS, - RUN_TEST_OFFICIAL, - TEST_WITH_OPT_DEPS, - env_var_2_bool, -) +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 if TEST_WITH_OPT_DEPS: - assert GITHUB_ACTIONS is True - assert (env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) or not GITHUB_ACTIONS) is True - assert os.getenv("TEST_WITH_OPT_DEPS", "false") == "true" - assert env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) is True import pytz