mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2025-01-04 18:12:12 +01:00
Bump APS & Deprecate pytz
Support (#4582)
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
This commit is contained in:
parent
5f35304e63
commit
d0a6e5141c
17 changed files with 190 additions and 99 deletions
3
.github/workflows/unit_tests.yml
vendored
3
.github/workflows/unit_tests.yml
vendored
|
@ -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.
|
||||
|
|
|
@ -158,7 +158,7 @@ PTB can be installed with optional dependencies:
|
|||
* ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1,<1.3 <https://aiolimiter.readthedocs.io/en/stable/>`_. Use this, if you want to use ``telegram.ext.AIORateLimiter``.
|
||||
* ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 <https://www.tornadoweb.org/en/stable/>`_ library. Use this, if you want to use ``telegram.ext.Updater.start_webhook``/``telegram.ext.Application.run_webhook``.
|
||||
* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<5.6.0 <https://cachetools.readthedocs.io/en/latest/>`_ library. Use this, if you want to use `arbitrary callback_data <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Arbitrary-callback_data>`_.
|
||||
* ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 <https://apscheduler.readthedocs.io/en/3.x/>`_ library and enforces `pytz>=2018.6 <https://pypi.org/project/pytz/>`_, where ``pytz`` is a dependency of ``APScheduler``. Use this, if you want to use the ``telegram.ext.JobQueue``.
|
||||
* ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 <https://apscheduler.readthedocs.io/en/3.x/>`_ library. Use this, if you want to use the ``telegram.ext.JobQueue``.
|
||||
|
||||
To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot[socks,webhooks]"``.
|
||||
|
||||
|
|
|
@ -76,9 +76,7 @@ http2 = [
|
|||
]
|
||||
job-queue = [
|
||||
# APS doesn't have a strict stability policy. Let's be cautious for now.
|
||||
"APScheduler~=3.10.4",
|
||||
# pytz is required by APS and just needs the lower bound due to #2120
|
||||
"pytz>=2018.6",
|
||||
"APScheduler>=3.10.4,<3.12.0",
|
||||
]
|
||||
passport = [
|
||||
"cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1",
|
||||
|
|
|
@ -16,4 +16,8 @@ pytest-xdist==3.6.1
|
|||
flaky>=3.8.1
|
||||
|
||||
# used in test_official for parsing tg docs
|
||||
beautifulsoup4
|
||||
beautifulsoup4
|
||||
|
||||
# For testing with timezones. Might not be needed on all systems, but to ensure that unit tests
|
||||
# run correctly on all systems, we include it here.
|
||||
tzdata
|
|
@ -27,6 +27,7 @@ Warning:
|
|||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import contextlib
|
||||
import datetime as dtm
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
@ -34,22 +35,26 @@ from typing import TYPE_CHECKING, Optional, Union
|
|||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
# pytz is only available if it was installed as dependency of APScheduler, so we make a little
|
||||
# workaround here
|
||||
DTM_UTC = dtm.timezone.utc
|
||||
UTC = dtm.timezone.utc
|
||||
try:
|
||||
import pytz
|
||||
|
||||
UTC = pytz.utc
|
||||
except ImportError:
|
||||
UTC = DTM_UTC # type: ignore[assignment]
|
||||
pytz = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime:
|
||||
"""Localize the datetime, where UTC is handled depending on whether pytz is available or not"""
|
||||
if tzinfo is DTM_UTC:
|
||||
return datetime.replace(tzinfo=DTM_UTC)
|
||||
return tzinfo.localize(datetime) # type: ignore[attr-defined]
|
||||
def localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime:
|
||||
"""Localize the datetime, both for pytz and zoneinfo timezones."""
|
||||
if tzinfo is UTC:
|
||||
return datetime.replace(tzinfo=UTC)
|
||||
|
||||
with contextlib.suppress(AttributeError):
|
||||
# Since pytz might not be available, we need the suppress context manager
|
||||
if isinstance(tzinfo, pytz.BaseTzInfo):
|
||||
return tzinfo.localize(datetime)
|
||||
|
||||
if datetime.tzinfo is None:
|
||||
return datetime.replace(tzinfo=tzinfo)
|
||||
return datetime.astimezone(tzinfo)
|
||||
|
||||
|
||||
def to_float_timestamp(
|
||||
|
@ -87,7 +92,7 @@ def to_float_timestamp(
|
|||
will be raised.
|
||||
tzinfo (:class:`datetime.tzinfo`, optional): If :paramref:`time_object` is a naive object
|
||||
from the :mod:`datetime` module, it will be interpreted as this timezone. Defaults to
|
||||
``pytz.utc``, if available, and :attr:`datetime.timezone.utc` otherwise.
|
||||
:attr:`datetime.timezone.utc` otherwise.
|
||||
|
||||
Note:
|
||||
Only to be used by ``telegram.ext``.
|
||||
|
@ -121,6 +126,12 @@ def to_float_timestamp(
|
|||
return reference_timestamp + time_object
|
||||
|
||||
if tzinfo is None:
|
||||
# We do this here rather than in the signature to ensure that we can make calls like
|
||||
# to_float_timestamp(
|
||||
# time, tzinfo=bot.defaults.tzinfo if bot.defaults else None
|
||||
# )
|
||||
# This ensures clean separation of concerns, i.e. the default timezone should not be
|
||||
# the responsibility of the caller
|
||||
tzinfo = UTC
|
||||
|
||||
if isinstance(time_object, dtm.time):
|
||||
|
@ -132,7 +143,9 @@ def to_float_timestamp(
|
|||
|
||||
aware_datetime = dtm.datetime.combine(reference_date, time_object)
|
||||
if aware_datetime.tzinfo is None:
|
||||
aware_datetime = _localize(aware_datetime, tzinfo)
|
||||
# datetime.combine uses the tzinfo of `time_object`, which might be None
|
||||
# so we still need to localize
|
||||
aware_datetime = localize(aware_datetime, tzinfo)
|
||||
|
||||
# if the time of day has passed today, use tomorrow
|
||||
if reference_time > aware_datetime.timetz():
|
||||
|
@ -140,7 +153,7 @@ def to_float_timestamp(
|
|||
return _datetime_to_float_timestamp(aware_datetime)
|
||||
if isinstance(time_object, dtm.datetime):
|
||||
if time_object.tzinfo is None:
|
||||
time_object = _localize(time_object, tzinfo)
|
||||
time_object = localize(time_object, tzinfo)
|
||||
return _datetime_to_float_timestamp(time_object)
|
||||
|
||||
raise TypeError(f"Unable to convert {type(time_object).__name__} object to timestamp")
|
||||
|
|
|
@ -57,10 +57,13 @@ class Defaults:
|
|||
versions.
|
||||
tzinfo (:class:`datetime.tzinfo`, optional): A timezone to be used for all date(time)
|
||||
inputs appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
|
||||
somewhere, it will be assumed to be in :paramref:`tzinfo`. If the
|
||||
:class:`telegram.ext.JobQueue` is used, this must be a timezone provided
|
||||
by the ``pytz`` module. Defaults to ``pytz.utc``, if available, and
|
||||
somewhere, it will be assumed to be in :paramref:`tzinfo`. Defaults to
|
||||
:attr:`datetime.timezone.utc` otherwise.
|
||||
|
||||
.. deprecated:: NEXT.VERSION
|
||||
Support for ``pytz`` timezones is deprecated and will be removed in future
|
||||
versions.
|
||||
|
||||
block (:obj:`bool`, optional): Default setting for the :paramref:`BaseHandler.block`
|
||||
parameter
|
||||
of handlers and error handlers registered through :meth:`Application.add_handler` and
|
||||
|
@ -148,6 +151,19 @@ class Defaults:
|
|||
self._block: bool = block
|
||||
self._protect_content: Optional[bool] = protect_content
|
||||
|
||||
if "pytz" in str(self._tzinfo.__class__):
|
||||
# TODO: When dropping support, make sure to update _utils.datetime accordingly
|
||||
warn(
|
||||
message=PTBDeprecationWarning(
|
||||
version="NEXT.VERSION",
|
||||
message=(
|
||||
"Support for pytz timezones is deprecated and will be removed in "
|
||||
"future versions."
|
||||
),
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if disable_web_page_preview is not None and link_preview_options is not None:
|
||||
raise ValueError(
|
||||
"`disable_web_page_preview` and `link_preview_options` are mutually exclusive."
|
||||
|
|
|
@ -23,7 +23,6 @@ import weakref
|
|||
from typing import TYPE_CHECKING, Any, Generic, Optional, Union, cast, overload
|
||||
|
||||
try:
|
||||
import pytz
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
|
@ -31,6 +30,7 @@ try:
|
|||
except ImportError:
|
||||
APS_AVAILABLE = False
|
||||
|
||||
from telegram._utils.datetime import UTC, localize
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import JSONDict
|
||||
|
@ -155,13 +155,13 @@ class JobQueue(Generic[CCT]):
|
|||
dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary.
|
||||
|
||||
"""
|
||||
timezone: object = pytz.utc
|
||||
timezone: dtm.tzinfo = UTC
|
||||
if (
|
||||
self._application
|
||||
and isinstance(self.application.bot, ExtBot)
|
||||
and self.application.bot.defaults
|
||||
):
|
||||
timezone = self.application.bot.defaults.tzinfo or pytz.utc
|
||||
timezone = self.application.bot.defaults.tzinfo or UTC
|
||||
|
||||
return {
|
||||
"timezone": timezone,
|
||||
|
@ -197,8 +197,10 @@ class JobQueue(Generic[CCT]):
|
|||
dtm.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
|
||||
)
|
||||
if date_time.tzinfo is None:
|
||||
date_time = self.scheduler.timezone.localize(date_time)
|
||||
if shift_day and date_time <= dtm.datetime.now(pytz.utc):
|
||||
# dtm.combine uses the tzinfo of `time`, which might be None, so we still have
|
||||
# to localize it
|
||||
date_time = localize(date_time, self.scheduler.timezone)
|
||||
if shift_day and date_time <= dtm.datetime.now(UTC):
|
||||
date_time += dtm.timedelta(days=1)
|
||||
return date_time
|
||||
return time
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import datetime as dtm
|
||||
import time
|
||||
import zoneinfo
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -55,18 +56,38 @@ with the TEST_WITH_OPT_DEPS=False environment variable in addition to the regula
|
|||
|
||||
|
||||
class TestDatetime:
|
||||
@staticmethod
|
||||
def localize(dt, tzinfo):
|
||||
if TEST_WITH_OPT_DEPS:
|
||||
return tzinfo.localize(dt)
|
||||
return dt.replace(tzinfo=tzinfo)
|
||||
def test_localize_utc(self):
|
||||
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
|
||||
localized_dt = tg_dtm.localize(dt, tg_dtm.UTC)
|
||||
assert localized_dt.tzinfo == tg_dtm.UTC
|
||||
assert localized_dt == dt.replace(tzinfo=tg_dtm.UTC)
|
||||
|
||||
def test_helpers_utc(self):
|
||||
# Here we just test, that we got the correct UTC variant
|
||||
if not TEST_WITH_OPT_DEPS:
|
||||
assert tg_dtm.UTC is tg_dtm.DTM_UTC
|
||||
else:
|
||||
assert tg_dtm.UTC is not tg_dtm.DTM_UTC
|
||||
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
|
||||
def test_localize_pytz(self):
|
||||
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
|
||||
import pytz
|
||||
|
||||
tzinfo = pytz.timezone("Europe/Berlin")
|
||||
localized_dt = tg_dtm.localize(dt, tzinfo)
|
||||
assert localized_dt.hour == dt.hour
|
||||
assert localized_dt.tzinfo is not None
|
||||
assert tzinfo.utcoffset(dt) is not None
|
||||
|
||||
def test_localize_zoneinfo_naive(self):
|
||||
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
|
||||
tzinfo = zoneinfo.ZoneInfo("Europe/Berlin")
|
||||
localized_dt = tg_dtm.localize(dt, tzinfo)
|
||||
assert localized_dt.hour == dt.hour
|
||||
assert localized_dt.tzinfo is not None
|
||||
assert tzinfo.utcoffset(dt) is not None
|
||||
|
||||
def test_localize_zoneinfo_aware(self):
|
||||
dt = dtm.datetime(2023, 1, 1, 12, 0, 0, tzinfo=dtm.timezone.utc)
|
||||
tzinfo = zoneinfo.ZoneInfo("Europe/Berlin")
|
||||
localized_dt = tg_dtm.localize(dt, tzinfo)
|
||||
assert localized_dt.hour == dt.hour + 1
|
||||
assert localized_dt.tzinfo is not None
|
||||
assert tzinfo.utcoffset(dt) is not None
|
||||
|
||||
def test_to_float_timestamp_absolute_naive(self):
|
||||
"""Conversion from timezone-naive datetime to timestamp.
|
||||
|
@ -75,20 +96,12 @@ class TestDatetime:
|
|||
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
|
||||
assert tg_dtm.to_float_timestamp(datetime) == 1573431976.1
|
||||
|
||||
def test_to_float_timestamp_absolute_naive_no_pytz(self, monkeypatch):
|
||||
"""Conversion from timezone-naive datetime to timestamp.
|
||||
Naive datetimes should be assumed to be in UTC.
|
||||
"""
|
||||
monkeypatch.setattr(tg_dtm, "UTC", tg_dtm.DTM_UTC)
|
||||
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
|
||||
assert tg_dtm.to_float_timestamp(datetime) == 1573431976.1
|
||||
|
||||
def test_to_float_timestamp_absolute_aware(self, timezone):
|
||||
"""Conversion from timezone-aware datetime to timestamp"""
|
||||
# we're parametrizing this with two different UTC offsets to exclude the possibility
|
||||
# of an xpass when the test is run in a timezone with the same UTC offset
|
||||
test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
|
||||
datetime = self.localize(test_datetime, timezone)
|
||||
datetime = tg_dtm.localize(test_datetime, timezone)
|
||||
assert (
|
||||
tg_dtm.to_float_timestamp(datetime)
|
||||
== 1573431976.1 - timezone.utcoffset(test_datetime).total_seconds()
|
||||
|
@ -126,7 +139,7 @@ class TestDatetime:
|
|||
ref_datetime = dtm.datetime(1970, 1, 1, 12)
|
||||
utc_offset = timezone.utcoffset(ref_datetime)
|
||||
ref_t, time_of_day = tg_dtm._datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
|
||||
aware_time_of_day = self.localize(ref_datetime, timezone).timetz()
|
||||
aware_time_of_day = tg_dtm.localize(ref_datetime, timezone).timetz()
|
||||
|
||||
# first test that naive time is assumed to be utc:
|
||||
assert tg_dtm.to_float_timestamp(time_of_day, ref_t) == pytest.approx(ref_t)
|
||||
|
@ -169,7 +182,7 @@ class TestDatetime:
|
|||
# we're parametrizing this with two different UTC offsets to exclude the possibility
|
||||
# of an xpass when the test is run in a timezone with the same UTC offset
|
||||
test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
|
||||
datetime = self.localize(test_datetime, timezone)
|
||||
datetime = tg_dtm.localize(test_datetime, timezone)
|
||||
assert (
|
||||
tg_dtm.from_timestamp(1573431976.1 - timezone.utcoffset(test_datetime).total_seconds())
|
||||
== datetime
|
||||
|
|
|
@ -21,6 +21,7 @@ import datetime as dtm
|
|||
import functools
|
||||
import inspect
|
||||
import re
|
||||
import zoneinfo
|
||||
from collections.abc import Collection, Iterable
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
|
@ -40,15 +41,11 @@ from telegram import (
|
|||
Sticker,
|
||||
TelegramObject,
|
||||
)
|
||||
from telegram._utils.datetime import to_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
from telegram.constants import InputMediaType
|
||||
from telegram.ext import Defaults, ExtBot
|
||||
from telegram.request import RequestData
|
||||
from tests.auxil.envvars import TEST_WITH_OPT_DEPS
|
||||
|
||||
if TEST_WITH_OPT_DEPS:
|
||||
import pytz
|
||||
|
||||
|
||||
FORWARD_REF_PATTERN = re.compile(r"ForwardRef\('(?P<class_name>\w+)'\)")
|
||||
""" A pattern to find a class name in a ForwardRef typing annotation.
|
||||
|
@ -344,10 +341,10 @@ def build_kwargs(
|
|||
# Some special casing for methods that have "exactly one of the optionals" type args
|
||||
elif name in ["location", "contact", "venue", "inline_message_id"]:
|
||||
kws[name] = True
|
||||
elif name == "until_date":
|
||||
elif name.endswith("_date"):
|
||||
if manually_passed_value not in [None, DEFAULT_NONE]:
|
||||
# Europe/Berlin
|
||||
kws[name] = pytz.timezone("Europe/Berlin").localize(dtm.datetime(2000, 1, 1, 0))
|
||||
kws[name] = dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"))
|
||||
else:
|
||||
# naive UTC
|
||||
kws[name] = dtm.datetime(2000, 1, 1, 0)
|
||||
|
@ -395,6 +392,15 @@ def make_assertion_for_link_preview_options(
|
|||
)
|
||||
|
||||
|
||||
_EUROPE_BERLIN_TS = to_timestamp(
|
||||
dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"))
|
||||
)
|
||||
_UTC_TS = to_timestamp(dtm.datetime(2000, 1, 1, 0), tzinfo=zoneinfo.ZoneInfo("UTC"))
|
||||
_AMERICA_NEW_YORK_TS = to_timestamp(
|
||||
dtm.datetime(2000, 1, 1, 0, tzinfo=zoneinfo.ZoneInfo("America/New_York"))
|
||||
)
|
||||
|
||||
|
||||
async def make_assertion(
|
||||
url,
|
||||
request_data: RequestData,
|
||||
|
@ -530,14 +536,16 @@ async def make_assertion(
|
|||
)
|
||||
|
||||
# Check datetime conversion
|
||||
until_date = data.pop("until_date", None)
|
||||
if until_date:
|
||||
if manual_value_expected and until_date != 946681200:
|
||||
pytest.fail("Non-naive until_date should have been interpreted as Europe/Berlin.")
|
||||
if not any((manually_passed_value, expected_defaults_value)) and until_date != 946684800:
|
||||
pytest.fail("Naive until_date should have been interpreted as UTC")
|
||||
if default_value_expected and until_date != 946702800:
|
||||
pytest.fail("Naive until_date should have been interpreted as America/New_York")
|
||||
date_keys = [key for key in data if key.endswith("_date")]
|
||||
for key in date_keys:
|
||||
date_param = data.pop(key)
|
||||
if date_param:
|
||||
if manual_value_expected and date_param != _EUROPE_BERLIN_TS:
|
||||
pytest.fail(f"Non-naive `{key}` should have been interpreted as Europe/Berlin.")
|
||||
if not any((manually_passed_value, expected_defaults_value)) and date_param != _UTC_TS:
|
||||
pytest.fail(f"Naive `{key}` should have been interpreted as UTC")
|
||||
if default_value_expected and date_param != _AMERICA_NEW_YORK_TS:
|
||||
pytest.fail(f"Naive `{key}` should have been interpreted as America/New_York")
|
||||
|
||||
if method_name in ["get_file", "get_small_file", "get_big_file"]:
|
||||
# This is here mainly for PassportFile.get_file, which calls .set_credentials on the
|
||||
|
@ -596,7 +604,7 @@ async def check_defaults_handling(
|
|||
|
||||
defaults_no_custom_defaults = Defaults()
|
||||
kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters}
|
||||
kwargs["tzinfo"] = pytz.timezone("America/New_York")
|
||||
kwargs["tzinfo"] = zoneinfo.ZoneInfo("America/New_York")
|
||||
kwargs.pop("disable_web_page_preview") # mutually exclusive with link_preview_options
|
||||
kwargs.pop("quote") # mutually exclusive with do_quote
|
||||
kwargs["link_preview_options"] = LinkPreviewOptions(
|
||||
|
|
|
@ -24,6 +24,8 @@ import random
|
|||
|
||||
from telegram._utils.strings import TextEncoding
|
||||
|
||||
from .envvars import GITHUB_ACTIONS
|
||||
|
||||
# Provide some public fallbacks so it's easy for contributors to run tests on their local machine
|
||||
# These bots are only able to talk in our test chats, so they are quite useless for other
|
||||
# purposes than testing.
|
||||
|
@ -41,10 +43,9 @@ FALLBACKS = (
|
|||
"NjcmlwdGlvbl9jaGFubmVsX2lkIjogLTEwMDIyMjk2NDkzMDN9XQ=="
|
||||
)
|
||||
|
||||
GITHUB_ACTION = os.getenv("GITHUB_ACTION", None)
|
||||
BOTS = os.getenv("BOTS", None)
|
||||
JOB_INDEX = os.getenv("JOB_INDEX", None)
|
||||
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
|
||||
if GITHUB_ACTIONS and BOTS is not None and JOB_INDEX is not None:
|
||||
BOTS = json.loads(base64.b64decode(BOTS).decode(TextEncoding.UTF_8))
|
||||
JOB_INDEX = int(JOB_INDEX)
|
||||
|
||||
|
@ -60,7 +61,7 @@ class BotInfoProvider:
|
|||
@staticmethod
|
||||
def _get_value(key, fallback):
|
||||
# If we're running as a github action then fetch bots from the repo secrets
|
||||
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
|
||||
if GITHUB_ACTIONS and BOTS is not None and JOB_INDEX is not None:
|
||||
try:
|
||||
return BOTS[JOB_INDEX][key]
|
||||
except (IndexError, KeyError):
|
||||
|
|
|
@ -27,6 +27,9 @@ def env_var_2_bool(env_var: object) -> bool:
|
|||
return env_var.lower().strip() == "true"
|
||||
|
||||
|
||||
GITHUB_ACTION = os.getenv("GITHUB_ACTION", "")
|
||||
TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "true"))
|
||||
RUN_TEST_OFFICIAL = env_var_2_bool(os.getenv("TEST_OFFICIAL"))
|
||||
GITHUB_ACTIONS: bool = env_var_2_bool(os.getenv("GITHUB_ACTIONS", "false"))
|
||||
TEST_WITH_OPT_DEPS: bool = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) or (
|
||||
# on local setups, we usually want to test with optional dependencies
|
||||
not GITHUB_ACTIONS
|
||||
)
|
||||
RUN_TEST_OFFICIAL: bool = env_var_2_bool(os.getenv("TEST_OFFICIAL"))
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import asyncio
|
||||
import datetime as dtm
|
||||
import logging
|
||||
import sys
|
||||
import zoneinfo
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
|
@ -40,11 +40,10 @@ from telegram.ext import Defaults
|
|||
from tests.auxil.build_messages import DATE
|
||||
from tests.auxil.ci_bots import BOT_INFO_PROVIDER, JOB_INDEX
|
||||
from tests.auxil.constants import PRIVATE_KEY, TEST_TOPIC_ICON_COLOR, TEST_TOPIC_NAME
|
||||
from tests.auxil.envvars import GITHUB_ACTION, RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS
|
||||
from tests.auxil.envvars import GITHUB_ACTIONS, RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS
|
||||
from tests.auxil.files import data_file
|
||||
from tests.auxil.networking import NonchalantHttpxRequest
|
||||
from tests.auxil.pytest_classes import PytestBot, make_bot
|
||||
from tests.auxil.timezones import BasicTimezone
|
||||
|
||||
if TEST_WITH_OPT_DEPS:
|
||||
import pytz
|
||||
|
@ -97,7 +96,7 @@ def pytest_collection_modifyitems(items: list[pytest.Item]):
|
|||
parent.add_marker(pytest.mark.no_req)
|
||||
|
||||
|
||||
if GITHUB_ACTION and JOB_INDEX == 0:
|
||||
if GITHUB_ACTIONS and JOB_INDEX == 0:
|
||||
# let's not slow down the tests too much with these additional checks
|
||||
# that's why we run them only in GitHub actions and only on *one* of the several test
|
||||
# matrix entries
|
||||
|
@ -308,12 +307,20 @@ def false_update(request):
|
|||
return Update(update_id=1, **request.param)
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="session",
|
||||
params=[pytz.timezone, zoneinfo.ZoneInfo] if TEST_WITH_OPT_DEPS else [zoneinfo.ZoneInfo],
|
||||
)
|
||||
def _tz_implementation(request): # noqa: PT005
|
||||
# This fixture is used to parametrize the timezone fixture
|
||||
# This is similar to what @pyttest.mark.parametrize does but for fixtures
|
||||
# However, this is needed only internally for the `tzinfo` fixture, so we keep it private
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", params=["Europe/Berlin", "Asia/Singapore", "UTC"])
|
||||
def tzinfo(request):
|
||||
if TEST_WITH_OPT_DEPS:
|
||||
return pytz.timezone(request.param)
|
||||
hours_offset = {"Europe/Berlin": 2, "Asia/Singapore": 8, "UTC": 0}[request.param]
|
||||
return BasicTimezone(offset=dtm.timedelta(hours=hours_offset), name=request.param)
|
||||
def tzinfo(request, _tz_implementation):
|
||||
return _tz_implementation(request.param)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
|
|
@ -38,10 +38,17 @@ class TestDefaults:
|
|||
|
||||
def test_utc(self):
|
||||
defaults = Defaults()
|
||||
if not TEST_WITH_OPT_DEPS:
|
||||
assert defaults.tzinfo is dtm.timezone.utc
|
||||
else:
|
||||
assert defaults.tzinfo is not dtm.timezone.utc
|
||||
assert defaults.tzinfo is dtm.timezone.utc
|
||||
|
||||
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
|
||||
def test_pytz_deprecation(self, recwarn):
|
||||
import pytz
|
||||
|
||||
with pytest.warns(PTBDeprecationWarning, match="pytz") as record:
|
||||
Defaults(tzinfo=pytz.timezone("Europe/Berlin"))
|
||||
|
||||
assert record[0].category == PTBDeprecationWarning
|
||||
assert record[0].filename == __file__, "wrong stacklevel!"
|
||||
|
||||
def test_data_assignment(self):
|
||||
defaults = Defaults()
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import asyncio
|
||||
import calendar
|
||||
import contextlib
|
||||
import datetime as dtm
|
||||
import logging
|
||||
import platform
|
||||
|
@ -26,7 +27,7 @@ import time
|
|||
import pytest
|
||||
|
||||
from telegram.ext import ApplicationBuilder, CallbackContext, ContextTypes, Defaults, Job, JobQueue
|
||||
from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS
|
||||
from tests.auxil.envvars import GITHUB_ACTIONS, TEST_WITH_OPT_DEPS
|
||||
from tests.auxil.pytest_classes import make_bot
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
@ -68,7 +69,7 @@ class TestNoJobQueue:
|
|||
not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed"
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
bool(GITHUB_ACTION and platform.system() in ["Windows", "Darwin"]),
|
||||
GITHUB_ACTIONS and platform.system() in ["Windows", "Darwin"],
|
||||
reason="On Windows & MacOS precise timings are not accurate.",
|
||||
)
|
||||
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect
|
||||
|
@ -77,6 +78,13 @@ class TestJobQueue:
|
|||
job_time = 0
|
||||
received_error = None
|
||||
|
||||
@staticmethod
|
||||
def normalize(datetime: dtm.datetime, timezone: dtm.tzinfo) -> dtm.datetime:
|
||||
with contextlib.suppress(AttributeError):
|
||||
return timezone.normalize(datetime)
|
||||
|
||||
return datetime
|
||||
|
||||
async def test_repr(self, app):
|
||||
jq = JobQueue()
|
||||
jq.set_application(app)
|
||||
|
@ -102,7 +110,7 @@ class TestJobQueue:
|
|||
# Unfortunately, we can't really test the executor setting explicitly without relying
|
||||
# on protected attributes. However, this should be tested enough implicitly via all the
|
||||
# other tests in here
|
||||
assert job_queue.scheduler_configuration["timezone"] is UTC
|
||||
assert job_queue.scheduler_configuration["timezone"] is dtm.timezone.utc
|
||||
|
||||
tz_app = ApplicationBuilder().defaults(Defaults(tzinfo=timezone)).token(bot.token).build()
|
||||
assert tz_app.job_queue.scheduler_configuration["timezone"] is timezone
|
||||
|
@ -356,6 +364,17 @@ class TestJobQueue:
|
|||
scheduled_time = job_queue.jobs()[0].next_t.timestamp()
|
||||
assert scheduled_time == pytest.approx(expected_time)
|
||||
|
||||
async def test_time_unit_dt_aware_time(self, job_queue, timezone):
|
||||
# Testing running at a specific tz-aware time today
|
||||
delta, now = 0.5, dtm.datetime.now(timezone)
|
||||
expected_time = now + dtm.timedelta(seconds=delta)
|
||||
when = expected_time.timetz()
|
||||
expected_time = expected_time.timestamp()
|
||||
|
||||
job_queue.run_once(self.job_datetime_tests, when)
|
||||
await asyncio.sleep(0.6)
|
||||
assert self.job_time == pytest.approx(expected_time)
|
||||
|
||||
async def test_run_daily(self, job_queue):
|
||||
delta, now = 1, dtm.datetime.now(UTC)
|
||||
time_of_day = (now + dtm.timedelta(seconds=delta)).time()
|
||||
|
@ -397,7 +416,7 @@ class TestJobQueue:
|
|||
if day > next_months_days:
|
||||
expected_reschedule_time += dtm.timedelta(next_months_days)
|
||||
|
||||
expected_reschedule_time = timezone.normalize(expected_reschedule_time)
|
||||
expected_reschedule_time = self.normalize(expected_reschedule_time, timezone)
|
||||
# Adjust the hour for the special case that between now and next month a DST switch happens
|
||||
expected_reschedule_time += dtm.timedelta(
|
||||
hours=time_of_day.hour - expected_reschedule_time.hour
|
||||
|
@ -419,7 +438,7 @@ class TestJobQueue:
|
|||
calendar.monthrange(now.year, now.month)[1]
|
||||
) - dtm.timedelta(days=now.day)
|
||||
# Adjust the hour for the special case that between now & end of month a DST switch happens
|
||||
expected_reschedule_time = timezone.normalize(expected_reschedule_time)
|
||||
expected_reschedule_time = self.normalize(expected_reschedule_time, timezone)
|
||||
expected_reschedule_time += dtm.timedelta(
|
||||
hours=time_of_day.hour - expected_reschedule_time.hour
|
||||
)
|
||||
|
|
|
@ -35,7 +35,7 @@ from telegram.constants import ParseMode
|
|||
from telegram.error import RetryAfter
|
||||
from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot
|
||||
from telegram.request import BaseRequest, RequestData
|
||||
from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS
|
||||
from tests.auxil.envvars import GITHUB_ACTIONS, TEST_WITH_OPT_DEPS
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
@ -142,7 +142,7 @@ class TestBaseRateLimiter:
|
|||
not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed"
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
bool(GITHUB_ACTION and platform.system() == "Darwin"),
|
||||
GITHUB_ACTIONS and platform.system() == "Darwin",
|
||||
reason="The timings are apparently rather inaccurate on MacOS.",
|
||||
)
|
||||
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect
|
||||
|
|
|
@ -79,7 +79,7 @@ from telegram import (
|
|||
User,
|
||||
WebAppInfo,
|
||||
)
|
||||
from telegram._utils.datetime import UTC, from_timestamp, to_timestamp
|
||||
from telegram._utils.datetime import UTC, from_timestamp, localize, to_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.strings import to_camel_case
|
||||
from telegram.constants import (
|
||||
|
@ -97,7 +97,7 @@ from telegram.request import BaseRequest, HTTPXRequest, RequestData
|
|||
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
|
||||
from tests.auxil.bot_method_checks import check_defaults_handling
|
||||
from tests.auxil.ci_bots import FALLBACKS
|
||||
from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS
|
||||
from tests.auxil.envvars import GITHUB_ACTIONS
|
||||
from tests.auxil.files import data_file
|
||||
from tests.auxil.networking import OfflineRequest, expect_bad_request
|
||||
from tests.auxil.pytest_classes import PytestBot, PytestExtBot, make_bot
|
||||
|
@ -154,7 +154,7 @@ def inline_results():
|
|||
BASE_GAME_SCORE = 60 # Base game score for game tests
|
||||
|
||||
xfail = pytest.mark.xfail(
|
||||
bool(GITHUB_ACTION), # This condition is only relevant for github actions game tests.
|
||||
GITHUB_ACTIONS, # This condition is only relevant for github actions game tests.
|
||||
reason=(
|
||||
"Can fail due to race conditions when multiple test suites "
|
||||
"with the same bot token are run at the same time"
|
||||
|
@ -3467,7 +3467,6 @@ class TestBotWithRequest:
|
|||
)
|
||||
assert revoked_link.is_revoked
|
||||
|
||||
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="This test's implementation requires pytz")
|
||||
@pytest.mark.parametrize("datetime", argvalues=[True, False], ids=["datetime", "integer"])
|
||||
async def test_advanced_chat_invite_links(self, bot, channel_id, datetime):
|
||||
# we are testing this all in one function in order to save api calls
|
||||
|
@ -3475,7 +3474,7 @@ class TestBotWithRequest:
|
|||
add_seconds = dtm.timedelta(0, 70)
|
||||
time_in_future = timestamp + add_seconds
|
||||
expire_time = time_in_future if datetime else to_timestamp(time_in_future)
|
||||
aware_time_in_future = UTC.localize(time_in_future)
|
||||
aware_time_in_future = localize(time_in_future, UTC)
|
||||
|
||||
invite_link = await bot.create_chat_invite_link(
|
||||
channel_id, expire_date=expire_time, member_limit=10
|
||||
|
@ -3488,7 +3487,7 @@ class TestBotWithRequest:
|
|||
add_seconds = dtm.timedelta(0, 80)
|
||||
time_in_future = timestamp + add_seconds
|
||||
expire_time = time_in_future if datetime else to_timestamp(time_in_future)
|
||||
aware_time_in_future = UTC.localize(time_in_future)
|
||||
aware_time_in_future = localize(time_in_future, UTC)
|
||||
|
||||
edited_invite_link = await bot.edit_chat_invite_link(
|
||||
channel_id,
|
||||
|
|
|
@ -58,7 +58,7 @@ class TestConstantsWithoutRequest:
|
|||
not key.startswith("_")
|
||||
# exclude imported stuff
|
||||
and getattr(member, "__module__", "telegram.constants") == "telegram.constants"
|
||||
and key not in ("sys", "dtm")
|
||||
and key not in ("sys", "dtm", "UTC")
|
||||
)
|
||||
}
|
||||
actual = set(constants.__all__)
|
||||
|
|
Loading…
Reference in a new issue