mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2025-01-06 18:50:54 +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
|
# Test the rest
|
||||||
export TEST_WITH_OPT_DEPS='true'
|
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
|
# `-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
|
# workers. Increasing number of workers has little effect on test duration, but it seems
|
||||||
# to increase flakyness.
|
# 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[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[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[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]"``.
|
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 = [
|
job-queue = [
|
||||||
# APS doesn't have a strict stability policy. Let's be cautious for now.
|
# 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",
|
|
||||||
]
|
]
|
||||||
passport = [
|
passport = [
|
||||||
"cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1",
|
"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
|
flaky>=3.8.1
|
||||||
|
|
||||||
# used in test_official for parsing tg docs
|
# 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
|
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||||
the changelog.
|
the changelog.
|
||||||
"""
|
"""
|
||||||
|
import contextlib
|
||||||
import datetime as dtm
|
import datetime as dtm
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
@ -34,22 +35,26 @@ from typing import TYPE_CHECKING, Optional, Union
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from telegram import Bot
|
from telegram import Bot
|
||||||
|
|
||||||
# pytz is only available if it was installed as dependency of APScheduler, so we make a little
|
UTC = dtm.timezone.utc
|
||||||
# workaround here
|
|
||||||
DTM_UTC = dtm.timezone.utc
|
|
||||||
try:
|
try:
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
UTC = pytz.utc
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
UTC = DTM_UTC # type: ignore[assignment]
|
pytz = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
|
||||||
def _localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.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"""
|
"""Localize the datetime, both for pytz and zoneinfo timezones."""
|
||||||
if tzinfo is DTM_UTC:
|
if tzinfo is UTC:
|
||||||
return datetime.replace(tzinfo=DTM_UTC)
|
return datetime.replace(tzinfo=UTC)
|
||||||
return tzinfo.localize(datetime) # type: ignore[attr-defined]
|
|
||||||
|
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(
|
def to_float_timestamp(
|
||||||
|
@ -87,7 +92,7 @@ def to_float_timestamp(
|
||||||
will be raised.
|
will be raised.
|
||||||
tzinfo (:class:`datetime.tzinfo`, optional): If :paramref:`time_object` is a naive object
|
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
|
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:
|
Note:
|
||||||
Only to be used by ``telegram.ext``.
|
Only to be used by ``telegram.ext``.
|
||||||
|
@ -121,6 +126,12 @@ def to_float_timestamp(
|
||||||
return reference_timestamp + time_object
|
return reference_timestamp + time_object
|
||||||
|
|
||||||
if tzinfo is None:
|
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
|
tzinfo = UTC
|
||||||
|
|
||||||
if isinstance(time_object, dtm.time):
|
if isinstance(time_object, dtm.time):
|
||||||
|
@ -132,7 +143,9 @@ def to_float_timestamp(
|
||||||
|
|
||||||
aware_datetime = dtm.datetime.combine(reference_date, time_object)
|
aware_datetime = dtm.datetime.combine(reference_date, time_object)
|
||||||
if aware_datetime.tzinfo is None:
|
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 the time of day has passed today, use tomorrow
|
||||||
if reference_time > aware_datetime.timetz():
|
if reference_time > aware_datetime.timetz():
|
||||||
|
@ -140,7 +153,7 @@ def to_float_timestamp(
|
||||||
return _datetime_to_float_timestamp(aware_datetime)
|
return _datetime_to_float_timestamp(aware_datetime)
|
||||||
if isinstance(time_object, dtm.datetime):
|
if isinstance(time_object, dtm.datetime):
|
||||||
if time_object.tzinfo is None:
|
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)
|
return _datetime_to_float_timestamp(time_object)
|
||||||
|
|
||||||
raise TypeError(f"Unable to convert {type(time_object).__name__} object to timestamp")
|
raise TypeError(f"Unable to convert {type(time_object).__name__} object to timestamp")
|
||||||
|
|
|
@ -57,10 +57,13 @@ class Defaults:
|
||||||
versions.
|
versions.
|
||||||
tzinfo (:class:`datetime.tzinfo`, optional): A timezone to be used for all date(time)
|
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
|
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
|
somewhere, it will be assumed to be in :paramref:`tzinfo`. Defaults to
|
||||||
:class:`telegram.ext.JobQueue` is used, this must be a timezone provided
|
|
||||||
by the ``pytz`` module. Defaults to ``pytz.utc``, if available, and
|
|
||||||
:attr:`datetime.timezone.utc` otherwise.
|
: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`
|
block (:obj:`bool`, optional): Default setting for the :paramref:`BaseHandler.block`
|
||||||
parameter
|
parameter
|
||||||
of handlers and error handlers registered through :meth:`Application.add_handler` and
|
of handlers and error handlers registered through :meth:`Application.add_handler` and
|
||||||
|
@ -148,6 +151,19 @@ class Defaults:
|
||||||
self._block: bool = block
|
self._block: bool = block
|
||||||
self._protect_content: Optional[bool] = protect_content
|
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:
|
if disable_web_page_preview is not None and link_preview_options is not None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"`disable_web_page_preview` and `link_preview_options` are mutually exclusive."
|
"`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
|
from typing import TYPE_CHECKING, Any, Generic, Optional, Union, cast, overload
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pytz
|
|
||||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
@ -31,6 +30,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
APS_AVAILABLE = False
|
APS_AVAILABLE = False
|
||||||
|
|
||||||
|
from telegram._utils.datetime import UTC, localize
|
||||||
from telegram._utils.logging import get_logger
|
from telegram._utils.logging import get_logger
|
||||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||||
from telegram._utils.types import JSONDict
|
from telegram._utils.types import JSONDict
|
||||||
|
@ -155,13 +155,13 @@ class JobQueue(Generic[CCT]):
|
||||||
dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary.
|
dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
timezone: object = pytz.utc
|
timezone: dtm.tzinfo = UTC
|
||||||
if (
|
if (
|
||||||
self._application
|
self._application
|
||||||
and isinstance(self.application.bot, ExtBot)
|
and isinstance(self.application.bot, ExtBot)
|
||||||
and self.application.bot.defaults
|
and self.application.bot.defaults
|
||||||
):
|
):
|
||||||
timezone = self.application.bot.defaults.tzinfo or pytz.utc
|
timezone = self.application.bot.defaults.tzinfo or UTC
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timezone": timezone,
|
"timezone": timezone,
|
||||||
|
@ -197,8 +197,10 @@ class JobQueue(Generic[CCT]):
|
||||||
dtm.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
|
dtm.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
|
||||||
)
|
)
|
||||||
if date_time.tzinfo is None:
|
if date_time.tzinfo is None:
|
||||||
date_time = self.scheduler.timezone.localize(date_time)
|
# dtm.combine uses the tzinfo of `time`, which might be None, so we still have
|
||||||
if shift_day and date_time <= dtm.datetime.now(pytz.utc):
|
# 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)
|
date_time += dtm.timedelta(days=1)
|
||||||
return date_time
|
return date_time
|
||||||
return time
|
return time
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||||
import datetime as dtm
|
import datetime as dtm
|
||||||
import time
|
import time
|
||||||
|
import zoneinfo
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -55,18 +56,38 @@ with the TEST_WITH_OPT_DEPS=False environment variable in addition to the regula
|
||||||
|
|
||||||
|
|
||||||
class TestDatetime:
|
class TestDatetime:
|
||||||
@staticmethod
|
def test_localize_utc(self):
|
||||||
def localize(dt, tzinfo):
|
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
|
||||||
if TEST_WITH_OPT_DEPS:
|
localized_dt = tg_dtm.localize(dt, tg_dtm.UTC)
|
||||||
return tzinfo.localize(dt)
|
assert localized_dt.tzinfo == tg_dtm.UTC
|
||||||
return dt.replace(tzinfo=tzinfo)
|
assert localized_dt == dt.replace(tzinfo=tg_dtm.UTC)
|
||||||
|
|
||||||
def test_helpers_utc(self):
|
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
|
||||||
# Here we just test, that we got the correct UTC variant
|
def test_localize_pytz(self):
|
||||||
if not TEST_WITH_OPT_DEPS:
|
dt = dtm.datetime(2023, 1, 1, 12, 0, 0)
|
||||||
assert tg_dtm.UTC is tg_dtm.DTM_UTC
|
import pytz
|
||||||
else:
|
|
||||||
assert tg_dtm.UTC is not tg_dtm.DTM_UTC
|
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):
|
def test_to_float_timestamp_absolute_naive(self):
|
||||||
"""Conversion from timezone-naive datetime to timestamp.
|
"""Conversion from timezone-naive datetime to timestamp.
|
||||||
|
@ -75,20 +96,12 @@ class TestDatetime:
|
||||||
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
|
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
|
||||||
assert tg_dtm.to_float_timestamp(datetime) == 1573431976.1
|
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):
|
def test_to_float_timestamp_absolute_aware(self, timezone):
|
||||||
"""Conversion from timezone-aware datetime to timestamp"""
|
"""Conversion from timezone-aware datetime to timestamp"""
|
||||||
# we're parametrizing this with two different UTC offsets to exclude the possibility
|
# 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
|
# 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)
|
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 (
|
assert (
|
||||||
tg_dtm.to_float_timestamp(datetime)
|
tg_dtm.to_float_timestamp(datetime)
|
||||||
== 1573431976.1 - timezone.utcoffset(test_datetime).total_seconds()
|
== 1573431976.1 - timezone.utcoffset(test_datetime).total_seconds()
|
||||||
|
@ -126,7 +139,7 @@ class TestDatetime:
|
||||||
ref_datetime = dtm.datetime(1970, 1, 1, 12)
|
ref_datetime = dtm.datetime(1970, 1, 1, 12)
|
||||||
utc_offset = timezone.utcoffset(ref_datetime)
|
utc_offset = timezone.utcoffset(ref_datetime)
|
||||||
ref_t, time_of_day = tg_dtm._datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
|
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:
|
# 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)
|
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
|
# 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
|
# 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)
|
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 (
|
assert (
|
||||||
tg_dtm.from_timestamp(1573431976.1 - timezone.utcoffset(test_datetime).total_seconds())
|
tg_dtm.from_timestamp(1573431976.1 - timezone.utcoffset(test_datetime).total_seconds())
|
||||||
== datetime
|
== datetime
|
||||||
|
|
|
@ -21,6 +21,7 @@ import datetime as dtm
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
|
import zoneinfo
|
||||||
from collections.abc import Collection, Iterable
|
from collections.abc import Collection, Iterable
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
@ -40,15 +41,11 @@ from telegram import (
|
||||||
Sticker,
|
Sticker,
|
||||||
TelegramObject,
|
TelegramObject,
|
||||||
)
|
)
|
||||||
|
from telegram._utils.datetime import to_timestamp
|
||||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||||
from telegram.constants import InputMediaType
|
from telegram.constants import InputMediaType
|
||||||
from telegram.ext import Defaults, ExtBot
|
from telegram.ext import Defaults, ExtBot
|
||||||
from telegram.request import RequestData
|
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+)'\)")
|
FORWARD_REF_PATTERN = re.compile(r"ForwardRef\('(?P<class_name>\w+)'\)")
|
||||||
""" A pattern to find a class name in a ForwardRef typing annotation.
|
""" 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
|
# Some special casing for methods that have "exactly one of the optionals" type args
|
||||||
elif name in ["location", "contact", "venue", "inline_message_id"]:
|
elif name in ["location", "contact", "venue", "inline_message_id"]:
|
||||||
kws[name] = True
|
kws[name] = True
|
||||||
elif name == "until_date":
|
elif name.endswith("_date"):
|
||||||
if manually_passed_value not in [None, DEFAULT_NONE]:
|
if manually_passed_value not in [None, DEFAULT_NONE]:
|
||||||
# Europe/Berlin
|
# 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:
|
else:
|
||||||
# naive UTC
|
# naive UTC
|
||||||
kws[name] = dtm.datetime(2000, 1, 1, 0)
|
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(
|
async def make_assertion(
|
||||||
url,
|
url,
|
||||||
request_data: RequestData,
|
request_data: RequestData,
|
||||||
|
@ -530,14 +536,16 @@ async def make_assertion(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check datetime conversion
|
# Check datetime conversion
|
||||||
until_date = data.pop("until_date", None)
|
date_keys = [key for key in data if key.endswith("_date")]
|
||||||
if until_date:
|
for key in date_keys:
|
||||||
if manual_value_expected and until_date != 946681200:
|
date_param = data.pop(key)
|
||||||
pytest.fail("Non-naive until_date should have been interpreted as Europe/Berlin.")
|
if date_param:
|
||||||
if not any((manually_passed_value, expected_defaults_value)) and until_date != 946684800:
|
if manual_value_expected and date_param != _EUROPE_BERLIN_TS:
|
||||||
pytest.fail("Naive until_date should have been interpreted as UTC")
|
pytest.fail(f"Non-naive `{key}` should have been interpreted as Europe/Berlin.")
|
||||||
if default_value_expected and until_date != 946702800:
|
if not any((manually_passed_value, expected_defaults_value)) and date_param != _UTC_TS:
|
||||||
pytest.fail("Naive until_date should have been interpreted as America/New_York")
|
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"]:
|
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
|
# 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()
|
defaults_no_custom_defaults = Defaults()
|
||||||
kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters}
|
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("disable_web_page_preview") # mutually exclusive with link_preview_options
|
||||||
kwargs.pop("quote") # mutually exclusive with do_quote
|
kwargs.pop("quote") # mutually exclusive with do_quote
|
||||||
kwargs["link_preview_options"] = LinkPreviewOptions(
|
kwargs["link_preview_options"] = LinkPreviewOptions(
|
||||||
|
|
|
@ -24,6 +24,8 @@ import random
|
||||||
|
|
||||||
from telegram._utils.strings import TextEncoding
|
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
|
# 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
|
# These bots are only able to talk in our test chats, so they are quite useless for other
|
||||||
# purposes than testing.
|
# purposes than testing.
|
||||||
|
@ -41,10 +43,9 @@ FALLBACKS = (
|
||||||
"NjcmlwdGlvbl9jaGFubmVsX2lkIjogLTEwMDIyMjk2NDkzMDN9XQ=="
|
"NjcmlwdGlvbl9jaGFubmVsX2lkIjogLTEwMDIyMjk2NDkzMDN9XQ=="
|
||||||
)
|
)
|
||||||
|
|
||||||
GITHUB_ACTION = os.getenv("GITHUB_ACTION", None)
|
|
||||||
BOTS = os.getenv("BOTS", None)
|
BOTS = os.getenv("BOTS", None)
|
||||||
JOB_INDEX = os.getenv("JOB_INDEX", 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))
|
BOTS = json.loads(base64.b64decode(BOTS).decode(TextEncoding.UTF_8))
|
||||||
JOB_INDEX = int(JOB_INDEX)
|
JOB_INDEX = int(JOB_INDEX)
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ class BotInfoProvider:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_value(key, fallback):
|
def _get_value(key, fallback):
|
||||||
# If we're running as a github action then fetch bots from the repo secrets
|
# 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:
|
try:
|
||||||
return BOTS[JOB_INDEX][key]
|
return BOTS[JOB_INDEX][key]
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
|
|
|
@ -27,6 +27,9 @@ def env_var_2_bool(env_var: object) -> bool:
|
||||||
return env_var.lower().strip() == "true"
|
return env_var.lower().strip() == "true"
|
||||||
|
|
||||||
|
|
||||||
GITHUB_ACTION = os.getenv("GITHUB_ACTION", "")
|
GITHUB_ACTIONS: bool = env_var_2_bool(os.getenv("GITHUB_ACTIONS", "false"))
|
||||||
TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "true"))
|
TEST_WITH_OPT_DEPS: bool = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "false")) or (
|
||||||
RUN_TEST_OFFICIAL = env_var_2_bool(os.getenv("TEST_OFFICIAL"))
|
# 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
|
# You should have received a copy of the GNU Lesser Public License
|
||||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime as dtm
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import zoneinfo
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
@ -40,11 +40,10 @@ from telegram.ext import Defaults
|
||||||
from tests.auxil.build_messages import DATE
|
from tests.auxil.build_messages import DATE
|
||||||
from tests.auxil.ci_bots import BOT_INFO_PROVIDER, JOB_INDEX
|
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.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.files import data_file
|
||||||
from tests.auxil.networking import NonchalantHttpxRequest
|
from tests.auxil.networking import NonchalantHttpxRequest
|
||||||
from tests.auxil.pytest_classes import PytestBot, make_bot
|
from tests.auxil.pytest_classes import PytestBot, make_bot
|
||||||
from tests.auxil.timezones import BasicTimezone
|
|
||||||
|
|
||||||
if TEST_WITH_OPT_DEPS:
|
if TEST_WITH_OPT_DEPS:
|
||||||
import pytz
|
import pytz
|
||||||
|
@ -97,7 +96,7 @@ def pytest_collection_modifyitems(items: list[pytest.Item]):
|
||||||
parent.add_marker(pytest.mark.no_req)
|
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
|
# 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
|
# that's why we run them only in GitHub actions and only on *one* of the several test
|
||||||
# matrix entries
|
# matrix entries
|
||||||
|
@ -308,12 +307,20 @@ def false_update(request):
|
||||||
return Update(update_id=1, **request.param)
|
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"])
|
@pytest.fixture(scope="session", params=["Europe/Berlin", "Asia/Singapore", "UTC"])
|
||||||
def tzinfo(request):
|
def tzinfo(request, _tz_implementation):
|
||||||
if TEST_WITH_OPT_DEPS:
|
return _tz_implementation(request.param)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
|
|
@ -38,10 +38,17 @@ class TestDefaults:
|
||||||
|
|
||||||
def test_utc(self):
|
def test_utc(self):
|
||||||
defaults = Defaults()
|
defaults = Defaults()
|
||||||
if not TEST_WITH_OPT_DEPS:
|
assert defaults.tzinfo is dtm.timezone.utc
|
||||||
assert defaults.tzinfo is dtm.timezone.utc
|
|
||||||
else:
|
@pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed")
|
||||||
assert defaults.tzinfo is not dtm.timezone.utc
|
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):
|
def test_data_assignment(self):
|
||||||
defaults = Defaults()
|
defaults = Defaults()
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||||
import asyncio
|
import asyncio
|
||||||
import calendar
|
import calendar
|
||||||
|
import contextlib
|
||||||
import datetime as dtm
|
import datetime as dtm
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
|
@ -26,7 +27,7 @@ import time
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from telegram.ext import ApplicationBuilder, CallbackContext, ContextTypes, Defaults, Job, JobQueue
|
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.pytest_classes import make_bot
|
||||||
from tests.auxil.slots import mro_slots
|
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"
|
not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed"
|
||||||
)
|
)
|
||||||
@pytest.mark.skipif(
|
@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.",
|
reason="On Windows & MacOS precise timings are not accurate.",
|
||||||
)
|
)
|
||||||
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect
|
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect
|
||||||
|
@ -77,6 +78,13 @@ class TestJobQueue:
|
||||||
job_time = 0
|
job_time = 0
|
||||||
received_error = None
|
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):
|
async def test_repr(self, app):
|
||||||
jq = JobQueue()
|
jq = JobQueue()
|
||||||
jq.set_application(app)
|
jq.set_application(app)
|
||||||
|
@ -102,7 +110,7 @@ class TestJobQueue:
|
||||||
# Unfortunately, we can't really test the executor setting explicitly without relying
|
# 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
|
# on protected attributes. However, this should be tested enough implicitly via all the
|
||||||
# other tests in here
|
# 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()
|
tz_app = ApplicationBuilder().defaults(Defaults(tzinfo=timezone)).token(bot.token).build()
|
||||||
assert tz_app.job_queue.scheduler_configuration["timezone"] is timezone
|
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()
|
scheduled_time = job_queue.jobs()[0].next_t.timestamp()
|
||||||
assert scheduled_time == pytest.approx(expected_time)
|
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):
|
async def test_run_daily(self, job_queue):
|
||||||
delta, now = 1, dtm.datetime.now(UTC)
|
delta, now = 1, dtm.datetime.now(UTC)
|
||||||
time_of_day = (now + dtm.timedelta(seconds=delta)).time()
|
time_of_day = (now + dtm.timedelta(seconds=delta)).time()
|
||||||
|
@ -397,7 +416,7 @@ class TestJobQueue:
|
||||||
if day > next_months_days:
|
if day > next_months_days:
|
||||||
expected_reschedule_time += dtm.timedelta(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
|
# Adjust the hour for the special case that between now and next month a DST switch happens
|
||||||
expected_reschedule_time += dtm.timedelta(
|
expected_reschedule_time += dtm.timedelta(
|
||||||
hours=time_of_day.hour - expected_reschedule_time.hour
|
hours=time_of_day.hour - expected_reschedule_time.hour
|
||||||
|
@ -419,7 +438,7 @@ class TestJobQueue:
|
||||||
calendar.monthrange(now.year, now.month)[1]
|
calendar.monthrange(now.year, now.month)[1]
|
||||||
) - dtm.timedelta(days=now.day)
|
) - dtm.timedelta(days=now.day)
|
||||||
# Adjust the hour for the special case that between now & end of month a DST switch happens
|
# 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(
|
expected_reschedule_time += dtm.timedelta(
|
||||||
hours=time_of_day.hour - expected_reschedule_time.hour
|
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.error import RetryAfter
|
||||||
from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot
|
from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot
|
||||||
from telegram.request import BaseRequest, RequestData
|
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(
|
@pytest.mark.skipif(
|
||||||
|
@ -142,7 +142,7 @@ class TestBaseRateLimiter:
|
||||||
not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed"
|
not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed"
|
||||||
)
|
)
|
||||||
@pytest.mark.skipif(
|
@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.",
|
reason="The timings are apparently rather inaccurate on MacOS.",
|
||||||
)
|
)
|
||||||
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect
|
@pytest.mark.flaky(10, 1) # Timings aren't quite perfect
|
||||||
|
|
|
@ -79,7 +79,7 @@ from telegram import (
|
||||||
User,
|
User,
|
||||||
WebAppInfo,
|
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.defaultvalue import DEFAULT_NONE
|
||||||
from telegram._utils.strings import to_camel_case
|
from telegram._utils.strings import to_camel_case
|
||||||
from telegram.constants import (
|
from telegram.constants import (
|
||||||
|
@ -97,7 +97,7 @@ from telegram.request import BaseRequest, HTTPXRequest, RequestData
|
||||||
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
|
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
|
||||||
from tests.auxil.bot_method_checks import check_defaults_handling
|
from tests.auxil.bot_method_checks import check_defaults_handling
|
||||||
from tests.auxil.ci_bots import FALLBACKS
|
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.files import data_file
|
||||||
from tests.auxil.networking import OfflineRequest, expect_bad_request
|
from tests.auxil.networking import OfflineRequest, expect_bad_request
|
||||||
from tests.auxil.pytest_classes import PytestBot, PytestExtBot, make_bot
|
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
|
BASE_GAME_SCORE = 60 # Base game score for game tests
|
||||||
|
|
||||||
xfail = pytest.mark.xfail(
|
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=(
|
reason=(
|
||||||
"Can fail due to race conditions when multiple test suites "
|
"Can fail due to race conditions when multiple test suites "
|
||||||
"with the same bot token are run at the same time"
|
"with the same bot token are run at the same time"
|
||||||
|
@ -3467,7 +3467,6 @@ class TestBotWithRequest:
|
||||||
)
|
)
|
||||||
assert revoked_link.is_revoked
|
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"])
|
@pytest.mark.parametrize("datetime", argvalues=[True, False], ids=["datetime", "integer"])
|
||||||
async def test_advanced_chat_invite_links(self, bot, channel_id, datetime):
|
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
|
# 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)
|
add_seconds = dtm.timedelta(0, 70)
|
||||||
time_in_future = timestamp + add_seconds
|
time_in_future = timestamp + add_seconds
|
||||||
expire_time = time_in_future if datetime else to_timestamp(time_in_future)
|
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(
|
invite_link = await bot.create_chat_invite_link(
|
||||||
channel_id, expire_date=expire_time, member_limit=10
|
channel_id, expire_date=expire_time, member_limit=10
|
||||||
|
@ -3488,7 +3487,7 @@ class TestBotWithRequest:
|
||||||
add_seconds = dtm.timedelta(0, 80)
|
add_seconds = dtm.timedelta(0, 80)
|
||||||
time_in_future = timestamp + add_seconds
|
time_in_future = timestamp + add_seconds
|
||||||
expire_time = time_in_future if datetime else to_timestamp(time_in_future)
|
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(
|
edited_invite_link = await bot.edit_chat_invite_link(
|
||||||
channel_id,
|
channel_id,
|
||||||
|
|
|
@ -58,7 +58,7 @@ class TestConstantsWithoutRequest:
|
||||||
not key.startswith("_")
|
not key.startswith("_")
|
||||||
# exclude imported stuff
|
# exclude imported stuff
|
||||||
and getattr(member, "__module__", "telegram.constants") == "telegram.constants"
|
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__)
|
actual = set(constants.__all__)
|
||||||
|
|
Loading…
Reference in a new issue