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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy