Skip to content

Defaults.tzinfo #2042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Sep 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f43ede1
Temporarily enable tests for the v13 branch
Bibo-Joshi Jun 6, 2020
8e9938d
Refactor handling of kwargs in Bot methods (#1924)
Bibo-Joshi Jun 30, 2020
176d221
Refactor JobQueue (#1981)
Bibo-Joshi Jul 10, 2020
543440a
Refactor persistence of Bot instances (#1994)
Bibo-Joshi Jul 13, 2020
7867b9b
Extend rich comparison of objects (#1724)
Bibo-Joshi Jul 14, 2020
8e94d0d
Refactor handling of default_quote (#1965)
Bibo-Joshi Jul 19, 2020
4d4aeb2
Add tzinfo to Defaults
Bibo-Joshi Aug 4, 2020
389f95d
Rework dtm conversion for classes & some fine tuning
Bibo-Joshi Aug 5, 2020
194f36c
Temporarily enable tests for the v13 branch
Bibo-Joshi Jun 6, 2020
124c93f
Refactor handling of kwargs in Bot methods (#1924)
Bibo-Joshi Jun 30, 2020
fdc5670
Refactor JobQueue (#1981)
Bibo-Joshi Jul 10, 2020
5a94754
Refactor persistence of Bot instances (#1994)
Bibo-Joshi Jul 13, 2020
5f84f25
Extend rich comparison of objects (#1724)
Bibo-Joshi Jul 14, 2020
5ccc1b6
Refactor handling of default_quote (#1965)
Bibo-Joshi Jul 19, 2020
6d3b72a
Refactor Handling of Message VS Update Filters (#2032)
Bibo-Joshi Jul 28, 2020
8b09cf4
Update wording
Bibo-Joshi Aug 13, 2020
c0116cd
Let TG-class dates stay in UTC
Bibo-Joshi Aug 13, 2020
ba1e685
Merge branch 'v13' into default-tz
Bibo-Joshi Aug 13, 2020
dee672d
Temporarily enable tests for the v13 branch
Bibo-Joshi Jun 6, 2020
02b058c
Refactor handling of kwargs in Bot methods (#1924)
Bibo-Joshi Jun 30, 2020
958a41e
Refactor JobQueue (#1981)
Bibo-Joshi Jul 10, 2020
c3a426a
Refactor persistence of Bot instances (#1994)
Bibo-Joshi Jul 13, 2020
fe9370a
Extend rich comparison of objects (#1724)
Bibo-Joshi Jul 14, 2020
f74be43
Refactor handling of default_quote (#1965)
Bibo-Joshi Jul 19, 2020
87a426e
Refactor Handling of Message VS Update Filters (#2032)
Bibo-Joshi Jul 28, 2020
ad30a8f
Make context-based callbacks the default setting (#2050)
Bibo-Joshi Aug 16, 2020
380f2d6
Merge branch 'v13' into default-tz
Bibo-Joshi Aug 16, 2020
c3c2934
Temporarily enable tests for the v13 branch
Bibo-Joshi Jun 6, 2020
5f5993b
Refactor handling of kwargs in Bot methods (#1924)
Bibo-Joshi Jun 30, 2020
f132104
Refactor JobQueue (#1981)
Bibo-Joshi Jul 10, 2020
7fdd925
Refactor persistence of Bot instances (#1994)
Bibo-Joshi Jul 13, 2020
4dccf2c
Extend rich comparison of objects (#1724)
Bibo-Joshi Jul 14, 2020
cfa97b5
Refactor handling of default_quote (#1965)
Bibo-Joshi Jul 19, 2020
eaf0ba7
Refactor Handling of Message VS Update Filters (#2032)
Bibo-Joshi Jul 28, 2020
3993250
Make context-based callbacks the default setting (#2050)
Bibo-Joshi Aug 16, 2020
2dd481b
Merge branch 'v13' into default-tz
Bibo-Joshi Aug 24, 2020
d686aed
Merge branch 'v13' into default-tz
Bibo-Joshi Sep 13, 2020
a56385c
Minor cleanup, add some docs
Bibo-Joshi Sep 20, 2020
3f12abc
Minor tweaks
Bibo-Joshi Sep 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions telegram/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,8 @@ def kick_chat_member(self, chat_id, user_id, timeout=None, until_date=None, api_
until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will
be unbanned, unix time. If user is banned for more than 366 days or less than 30
seconds from the current time they are considered to be banned forever.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.

Expand All @@ -1729,7 +1731,8 @@ def kick_chat_member(self, chat_id, user_id, timeout=None, until_date=None, api_

if until_date is not None:
if isinstance(until_date, datetime):
until_date = to_timestamp(until_date)
until_date = to_timestamp(until_date,
tzinfo=self.defaults.tzinfo if self.defaults else None)
data['until_date'] = until_date

result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
Expand Down Expand Up @@ -2815,6 +2818,8 @@ def restrict_chat_member(self, chat_id, user_id, permissions, until_date=None,
will be lifted for the user, unix time. If user is restricted for more than 366
days or less than 30 seconds from the current time, they are considered to be
restricted forever.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used.
permissions (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user
permissions.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
Expand All @@ -2833,7 +2838,8 @@ def restrict_chat_member(self, chat_id, user_id, permissions, until_date=None,

if until_date is not None:
if isinstance(until_date, datetime):
until_date = to_timestamp(until_date)
until_date = to_timestamp(until_date,
tzinfo=self.defaults.tzinfo if self.defaults else None)
data['until_date'] = until_date

result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
Expand Down Expand Up @@ -3579,6 +3585,8 @@ def send_poll(self,
timestamp) when the poll will be automatically closed. Must be at least 5 and no
more than 600 seconds in the future. Can't be used together with
:attr:`open_period`.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used.
is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be
immediately closed. This can be useful for poll preview.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
Expand Down Expand Up @@ -3631,7 +3639,8 @@ def send_poll(self,
data['open_period'] = open_period
if close_date:
if isinstance(close_date, datetime):
close_date = to_timestamp(close_date)
close_date = to_timestamp(close_date,
tzinfo=self.defaults.tzinfo if self.defaults else None)
data['close_date'] = close_date

return self._message('sendPoll', data, timeout=timeout,
Expand Down
23 changes: 21 additions & 2 deletions telegram/ext/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the class Defaults, which allows to pass default values to Updater."""
import pytz

from telegram.utils.helpers import DEFAULT_NONE

Expand All @@ -37,6 +38,8 @@ class Defaults:
quote (:obj:`bool`): Optional. If set to :obj:`True`, the reply is sent as an actual reply
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
tzinfo (:obj:`tzinfo`): A timezone to be used for all date(time) objects appearing
throughout PTB.

Parameters:
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
Expand All @@ -51,6 +54,10 @@ class Defaults:
quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
tzinfo (:obj:`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 ``tzinfo``. Must be a timezone provided by the
``pytz`` module. Defaults to UTC.
"""
def __init__(self,
parse_mode=None,
Expand All @@ -59,12 +66,14 @@ def __init__(self,
# Timeout needs special treatment, since the bot methods have two different
# default values for timeout (None and 20s)
timeout=DEFAULT_NONE,
quote=None):
quote=None,
tzinfo=pytz.utc):
self._parse_mode = parse_mode
self._disable_notification = disable_notification
self._disable_web_page_preview = disable_web_page_preview
self._timeout = timeout
self._quote = quote
self._tzinfo = tzinfo

@property
def parse_mode(self):
Expand Down Expand Up @@ -111,12 +120,22 @@ def quote(self, value):
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")

@property
def tzinfo(self):
return self._tzinfo

@tzinfo.setter
def tzinfo(self, value):
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")

def __hash__(self):
return hash((self._parse_mode,
self._disable_notification,
self._disable_web_page_preview,
self._timeout,
self._quote))
self._quote,
self._tzinfo))

def __eq__(self, other):
if isinstance(other, Defaults):
Expand Down
33 changes: 16 additions & 17 deletions telegram/ext/jobqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ def _parse_time_input(self, time, shift_day=False):
return self._tz_now() + time
if isinstance(time, datetime.time):
dt = datetime.datetime.combine(
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time,
tzinfo=time.tzinfo or self.scheduler.timezone)
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time)
if dt.tzinfo is None:
dt = self.scheduler.timezone.localize(dt)
if shift_day and dt <= datetime.datetime.now(pytz.utc):
dt += datetime.timedelta(days=1)
return dt
Expand All @@ -106,6 +107,9 @@ def set_dispatcher(self, dispatcher):

"""
self._dispatcher = dispatcher
if dispatcher.bot.defaults:
if dispatcher.bot.defaults:
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)

def run_once(self, callback, when, context=None, name=None, job_kwargs=None):
"""Creates a new ``Job`` that runs once and adds it to the queue.
Expand All @@ -129,13 +133,11 @@ def run_once(self, callback, when, context=None, name=None, job_kwargs=None):
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
UTC will be assumed.
the default timezone of the bot will be used.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, UTC will be assumed.

If ``when`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
then ``when.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
default timezone of the bot will be used.

context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
Expand Down Expand Up @@ -193,13 +195,11 @@ def run_repeating(self, callback, interval, first=None, last=None, context=None,
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
UTC will be assumed.
the default timezone of the bot will be used.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, UTC will be assumed.

If ``first`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
then ``first.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
default timezone of the bot will be used.

Defaults to ``interval``
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
Expand All @@ -208,7 +208,8 @@ def run_repeating(self, callback, interval, first=None, last=None, context=None,
depending on its type. See ``first`` for details.

If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
and ``last.tzinfo`` is :obj:`None`, UTC will be assumed.
and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be
assumed.

Defaults to :obj:`None`.
context (:obj:`object`, optional): Additional data needed for the callback function.
Expand Down Expand Up @@ -268,8 +269,7 @@ def run_monthly(self, callback, when, day, context=None, name=None, day_is_stric
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
its ``job.context`` or change it to a repeating job.
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``when.tzinfo``) is :obj:`None`, UTC will be assumed. This will also implicitly
define ``Job.tzinfo``.
(``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
be within the range of 1 and 31, inclusive.
context (:obj:`object`, optional): Additional data needed for the callback function.
Expand Down Expand Up @@ -338,8 +338,7 @@ def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
its ``job.context`` or change it to a repeating job.
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``time.tzinfo``) is :obj:`None`, UTC will be assumed.
``time.tzinfo`` will implicitly define ``Job.tzinfo``.
(``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
run. Defaults to ``EVERY_DAY``
context (:obj:`object`, optional): Additional data needed for the callback function.
Expand Down
43 changes: 28 additions & 15 deletions telegram/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from html import escape
from numbers import Number

import pytz

try:
import ujson as json
except ImportError:
Expand Down Expand Up @@ -72,8 +74,6 @@ def escape_markdown(text, version=1, entity_type=None):


# -------- date/time related helpers --------
# TODO: add generic specification of UTC for naive datetimes to docs

def _datetime_to_float_timestamp(dt_obj):
"""
Converts a datetime object to a float timestamp (with sub-second precision).
Expand All @@ -85,13 +85,13 @@ def _datetime_to_float_timestamp(dt_obj):
return dt_obj.timestamp()


def to_float_timestamp(t, reference_timestamp=None):
def to_float_timestamp(t, reference_timestamp=None, tzinfo=None):
"""
Converts a given time object to a float POSIX timestamp.
Used to convert different time specifications to a common format. The time object
can be relative (i.e. indicate a time increment, or a time of day) or absolute.
Any objects from the :class:`datetime` module that are timezone-naive will be assumed
to be in UTC.
to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`.

:obj:`None` s are left alone (i.e. ``to_float_timestamp(None)`` is :obj:`None`).

Expand All @@ -113,6 +113,9 @@ def to_float_timestamp(t, reference_timestamp=None):
If ``t`` is given as an absolute representation of date & time (i.e. a
``datetime.datetime`` object), ``reference_timestamp`` is not relevant and so its
value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised.
tzinfo (:obj:`datetime.tzinfo`, optional): If ``t`` is a naive object from the
:class:`datetime` module, it will be interpreted as this timezone. Defaults to
``pytz.utc``.

Returns:
(float | None) The return value depends on the type of argument ``t``. If ``t`` is
Expand All @@ -138,33 +141,43 @@ def to_float_timestamp(t, reference_timestamp=None):
return reference_timestamp + t.total_seconds()
elif isinstance(t, Number):
return reference_timestamp + t
elif isinstance(t, dtm.time):
if t.tzinfo is not None:
reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo)
else:
reference_dt = dtm.datetime.utcfromtimestamp(reference_timestamp) # assume UTC

if tzinfo is None:
tzinfo = pytz.utc

if isinstance(t, dtm.time):
reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo or tzinfo)
reference_date = reference_dt.date()
reference_time = reference_dt.timetz()
if reference_time > t: # if the time of day has passed today, use tomorrow
reference_date += dtm.timedelta(days=1)
return _datetime_to_float_timestamp(dtm.datetime.combine(reference_date, t))

aware_datetime = dtm.datetime.combine(reference_date, t)
if aware_datetime.tzinfo is None:
aware_datetime = tzinfo.localize(aware_datetime)

# if the time of day has passed today, use tomorrow
if reference_time > aware_datetime.timetz():
aware_datetime += dtm.timedelta(days=1)
return _datetime_to_float_timestamp(aware_datetime)
elif isinstance(t, dtm.datetime):
if t.tzinfo is None:
t = tzinfo.localize(t)
return _datetime_to_float_timestamp(t)

raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__))


def to_timestamp(dt_obj, reference_timestamp=None):
def to_timestamp(dt_obj, reference_timestamp=None, tzinfo=pytz.utc):
"""
Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated
down to the nearest integer).

See the documentation for :func:`to_float_timestamp` for more details.
"""
return int(to_float_timestamp(dt_obj, reference_timestamp)) if dt_obj is not None else None
return (int(to_float_timestamp(dt_obj, reference_timestamp, tzinfo))
if dt_obj is not None else None)


def from_timestamp(unixtime, tzinfo=dtm.timezone.utc):
def from_timestamp(unixtime, tzinfo=pytz.utc):
"""
Converts an (integer) unix timestamp to a timezone aware datetime object.
:obj:`None`s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`).
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ def default_bot(request, bot_info):
return default_bot


@pytest.fixture(scope='function')
def tz_bot(timezone, bot_info):
defaults = Defaults(tzinfo=timezone)
default_bot = DEFAULT_BOTS.get(defaults)
if default_bot:
return default_bot
else:
default_bot = make_bot(bot_info, **{'defaults': defaults})
DEFAULT_BOTS[defaults] = default_bot
return default_bot


@pytest.fixture(scope='session')
def chat_id(bot_info):
return bot_info['chat_id']
Expand Down
Loading