mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-11-21 22:56:38 +01:00
Job.next_t (#1685)
* next_t property is added to Job class Added new property to Job class - next_t, it will show the datetime when the job will be executed next time. The property is updated during JobQueue._put method, right after job is added to queue. Related to #1676 * Fixed newline and trailing whitespace * Fixed PR issues, added test 1. Added setter for next_t - now JobQueue doesn't access protected Job._next_t. 2. Fixed Job class docstring. 3. Added test for next_t property. 4. Set next_t to None for run_once jobs that already ran. * Fixed Flake8 issues * Added next_t setter for datetime, added test 1. next_t setter now can accept datetime type. 2. added test for setting datetime to next_t and added some asserts that check tests results. 3. Also noticed Job.days setter raises ValueError when it's more appropriate to raise TypeError. * Fixed test_warnings, added Number type to next_t setter 1. Changed type of error raised by interval setter from ValueError to TypeError.. 2. Fixed test_warning after changing type of errors in Job.days and Job.interval. 3. Added Number type to next_t setter - now it can accept int too. * Python 2 compatibility for test_job_next_t_property Added _UTC and _UtcOffsetTimezone for python 2 compatibility * Fixed PR issues 1. Replaced "datetime.replace tzinfo" with "datetime.astimezone" 2. Moved testing next_t setter to separate test. 3. Changed test_job_next_t_setter so it now uses non UTC timezone. * Defining tzinfo from run_once, run_repeating 1. Added option to define Job.tzinfo from run_once (by when.tzinfo) and run_repeating (first.tzinfo) 2. Added test to check that tzinfo is always passed correctly. * address review Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
This commit is contained in:
parent
57546795c5
commit
110e2df443
3 changed files with 148 additions and 9 deletions
|
@ -18,6 +18,7 @@ The following wonderful people contributed directly or indirectly to this projec
|
|||
- `Alateas <https://github.com/alateas>`_
|
||||
- `Ales Dokshanin <https://github.com/alesdokshanin>`_
|
||||
- `Ambro17 <https://github.com/Ambro17>`_
|
||||
- `Andrej Zhilenkov <https://github.com/Andrej730>`_
|
||||
- `Anton Tagunov <https://github.com/anton-tagunov>`_
|
||||
- `Avanatiker <https://github.com/Avanatiker>`_
|
||||
- `Balduro <https://github.com/Balduro>`_
|
||||
|
|
|
@ -104,6 +104,7 @@ class JobQueue(object):
|
|||
# enqueue:
|
||||
self.logger.debug('Putting job %s with t=%s', job.name, time_spec)
|
||||
self._queue.put((next_t, job))
|
||||
job._set_next_t(next_t)
|
||||
|
||||
# Wake up the loop if this job should be executed next
|
||||
self._set_next_peek(next_t)
|
||||
|
@ -135,6 +136,9 @@ class JobQueue(object):
|
|||
job should run. This could be either today or, if the time has already passed,
|
||||
tomorrow. If the timezone (``time.tzinfo``) is ``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.
|
||||
|
||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
|
@ -145,7 +149,14 @@ class JobQueue(object):
|
|||
queue.
|
||||
|
||||
"""
|
||||
job = Job(callback, repeat=False, context=context, name=name, job_queue=self)
|
||||
tzinfo = when.tzinfo if isinstance(when, (datetime.datetime, datetime.time)) else None
|
||||
|
||||
job = Job(callback,
|
||||
repeat=False,
|
||||
context=context,
|
||||
name=name,
|
||||
job_queue=self,
|
||||
tzinfo=tzinfo)
|
||||
self._put(job, time_spec=when)
|
||||
return job
|
||||
|
||||
|
@ -179,6 +190,9 @@ class JobQueue(object):
|
|||
job should run. This could be either today or, if the time has already passed,
|
||||
tomorrow. If the timezone (``time.tzinfo``) is ``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.
|
||||
|
||||
Defaults to ``interval``
|
||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
|
||||
|
@ -195,12 +209,15 @@ class JobQueue(object):
|
|||
to pin servers to UTC time, then time related behaviour can always be expected.
|
||||
|
||||
"""
|
||||
tzinfo = first.tzinfo if isinstance(first, (datetime.datetime, datetime.time)) else None
|
||||
|
||||
job = Job(callback,
|
||||
interval=interval,
|
||||
repeat=True,
|
||||
context=context,
|
||||
name=name,
|
||||
job_queue=self)
|
||||
job_queue=self,
|
||||
tzinfo=tzinfo)
|
||||
self._put(job, time_spec=first)
|
||||
return job
|
||||
|
||||
|
@ -217,6 +234,7 @@ class JobQueue(object):
|
|||
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 ``None``, UTC will be assumed.
|
||||
``time.tzinfo`` will implicitly define ``Job.tzinfo``.
|
||||
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.
|
||||
|
@ -301,6 +319,7 @@ class JobQueue(object):
|
|||
if job.repeat and not job.removed:
|
||||
self._put(job, previous_t=t)
|
||||
else:
|
||||
job._set_next_t(None)
|
||||
self.logger.debug('Dropping non-repeating or removed job %s', job.name)
|
||||
|
||||
def start(self):
|
||||
|
@ -412,6 +431,7 @@ class Job(object):
|
|||
self._repeat = None
|
||||
self._interval = None
|
||||
self.interval = interval
|
||||
self._next_t = None
|
||||
self.repeat = repeat
|
||||
|
||||
self._days = None
|
||||
|
@ -438,6 +458,7 @@ class Job(object):
|
|||
|
||||
"""
|
||||
self._remove.set()
|
||||
self._next_t = None
|
||||
|
||||
@property
|
||||
def removed(self):
|
||||
|
@ -471,8 +492,8 @@ class Job(object):
|
|||
raise ValueError("The 'interval' can not be 'None' when 'repeat' is set to 'True'")
|
||||
|
||||
if not (interval is None or isinstance(interval, (Number, datetime.timedelta))):
|
||||
raise ValueError("The 'interval' must be of type 'datetime.timedelta',"
|
||||
" 'int' or 'float'")
|
||||
raise TypeError("The 'interval' must be of type 'datetime.timedelta',"
|
||||
" 'int' or 'float'")
|
||||
|
||||
self._interval = interval
|
||||
|
||||
|
@ -485,6 +506,27 @@ class Job(object):
|
|||
else:
|
||||
return interval
|
||||
|
||||
@property
|
||||
def next_t(self):
|
||||
"""
|
||||
::obj:`datetime.datetime`: Datetime for the next job execution.
|
||||
Datetime is localized according to :attr:`tzinfo`.
|
||||
If job is removed or already ran it equals to ``None``.
|
||||
|
||||
"""
|
||||
return datetime.datetime.fromtimestamp(self._next_t, self.tzinfo) if self._next_t else None
|
||||
|
||||
def _set_next_t(self, next_t):
|
||||
if isinstance(next_t, datetime.datetime):
|
||||
# Set timezone to UTC in case datetime is in local timezone.
|
||||
next_t = next_t.astimezone(_UTC)
|
||||
next_t = to_float_timestamp(next_t)
|
||||
elif not (isinstance(next_t, Number) or next_t is None):
|
||||
raise TypeError("The 'next_t' argument should be one of the following types: "
|
||||
"'float', 'int', 'datetime.datetime' or 'NoneType'")
|
||||
|
||||
self._next_t = next_t
|
||||
|
||||
@property
|
||||
def repeat(self):
|
||||
""":obj:`bool`: Optional. If this job should periodically execute its callback function."""
|
||||
|
@ -504,10 +546,10 @@ class Job(object):
|
|||
@days.setter
|
||||
def days(self, days):
|
||||
if not isinstance(days, tuple):
|
||||
raise ValueError("The 'days' argument should be of type 'tuple'")
|
||||
raise TypeError("The 'days' argument should be of type 'tuple'")
|
||||
|
||||
if not all(isinstance(day, int) for day in days):
|
||||
raise ValueError("The elements of the 'days' argument should be of type 'int'")
|
||||
raise TypeError("The elements of the 'days' argument should be of type 'int'")
|
||||
|
||||
if not all(0 <= day <= 6 for day in days):
|
||||
raise ValueError("The elements of the 'days' argument should be from 0 up to and "
|
||||
|
|
|
@ -16,6 +16,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/].
|
||||
|
||||
import datetime as dtm
|
||||
import os
|
||||
import sys
|
||||
|
@ -298,18 +299,21 @@ class TestJobQueue(object):
|
|||
with pytest.raises(ValueError, match='can not be'):
|
||||
j.interval = None
|
||||
j.repeat = False
|
||||
with pytest.raises(ValueError, match='must be of type'):
|
||||
with pytest.raises(TypeError, match='must be of type'):
|
||||
j.interval = 'every 3 minutes'
|
||||
j.interval = 15
|
||||
assert j.interval_seconds == 15
|
||||
|
||||
with pytest.raises(ValueError, match='argument should be of type'):
|
||||
with pytest.raises(TypeError, match='argument should be of type'):
|
||||
j.days = 'every day'
|
||||
with pytest.raises(ValueError, match='The elements of the'):
|
||||
with pytest.raises(TypeError, match='The elements of the'):
|
||||
j.days = ('mon', 'wed')
|
||||
with pytest.raises(ValueError, match='from 0 up to and'):
|
||||
j.days = (0, 6, 12, 14)
|
||||
|
||||
with pytest.raises(TypeError, match='argument should be one of the'):
|
||||
j._set_next_t('tomorrow')
|
||||
|
||||
def test_get_jobs(self, job_queue):
|
||||
job1 = job_queue.run_once(self.job_run_once, 10, name='name1')
|
||||
job2 = job_queue.run_once(self.job_run_once, 10, name='name1')
|
||||
|
@ -341,3 +345,95 @@ class TestJobQueue(object):
|
|||
|
||||
for job in jobs:
|
||||
assert job.tzinfo == _UTC
|
||||
|
||||
def test_job_next_t_property(self, job_queue):
|
||||
# Testing:
|
||||
# - next_t values match values from self._queue.queue (for run_once and run_repeating jobs)
|
||||
# - next_t equals None if job is removed or if it's already ran
|
||||
|
||||
job1 = job_queue.run_once(self.job_run_once, 0.06, name='run_once job')
|
||||
job2 = job_queue.run_once(self.job_run_once, 0.06, name='canceled run_once job')
|
||||
job_queue.run_repeating(self.job_run_once, 0.04, name='repeatable job')
|
||||
|
||||
sleep(0.05)
|
||||
job2.schedule_removal()
|
||||
|
||||
with job_queue._queue.mutex:
|
||||
for t, job in job_queue._queue.queue:
|
||||
t = dtm.datetime.fromtimestamp(t, job.tzinfo)
|
||||
|
||||
if job.removed:
|
||||
assert job.next_t is None
|
||||
else:
|
||||
assert job.next_t == t
|
||||
|
||||
assert self.result == 1
|
||||
sleep(0.02)
|
||||
|
||||
assert self.result == 2
|
||||
assert job1.next_t is None
|
||||
assert job2.next_t is None
|
||||
|
||||
def test_job_set_next_t(self, job_queue):
|
||||
# Testing next_t setter for 'datetime.datetime' values
|
||||
|
||||
job = job_queue.run_once(self.job_run_once, 0.05)
|
||||
|
||||
t = dtm.datetime.now(tz=_UtcOffsetTimezone(dtm.timedelta(hours=12)))
|
||||
job._set_next_t(t)
|
||||
job.tzinfo = _UtcOffsetTimezone(dtm.timedelta(hours=5))
|
||||
assert job.next_t == t.astimezone(job.tzinfo)
|
||||
|
||||
def test_passing_tzinfo_to_job(self, job_queue):
|
||||
"""Test that tzinfo is correctly passed to job with run_once, run_daily
|
||||
and run_repeating methods"""
|
||||
|
||||
when_dt_tz_specific = dtm.datetime.now(
|
||||
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
|
||||
) + dtm.timedelta(seconds=2)
|
||||
when_dt_tz_utc = dtm.datetime.now() + dtm.timedelta(seconds=2)
|
||||
job_once1 = job_queue.run_once(self.job_run_once, when_dt_tz_specific)
|
||||
job_once2 = job_queue.run_once(self.job_run_once, when_dt_tz_utc)
|
||||
|
||||
when_time_tz_specific = (dtm.datetime.now(
|
||||
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
|
||||
) + dtm.timedelta(seconds=2)).timetz()
|
||||
when_time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
|
||||
job_once3 = job_queue.run_once(self.job_run_once, when_time_tz_specific)
|
||||
job_once4 = job_queue.run_once(self.job_run_once, when_time_tz_utc)
|
||||
|
||||
first_dt_tz_specific = dtm.datetime.now(
|
||||
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
|
||||
) + dtm.timedelta(seconds=2)
|
||||
first_dt_tz_utc = dtm.datetime.now() + dtm.timedelta(seconds=2)
|
||||
job_repeating1 = job_queue.run_repeating(
|
||||
self.job_run_once, 2, first=first_dt_tz_specific)
|
||||
job_repeating2 = job_queue.run_repeating(
|
||||
self.job_run_once, 2, first=first_dt_tz_utc)
|
||||
|
||||
first_time_tz_specific = (dtm.datetime.now(
|
||||
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
|
||||
) + dtm.timedelta(seconds=2)).timetz()
|
||||
first_time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
|
||||
job_repeating3 = job_queue.run_repeating(
|
||||
self.job_run_once, 2, first=first_time_tz_specific)
|
||||
job_repeating4 = job_queue.run_repeating(
|
||||
self.job_run_once, 2, first=first_time_tz_utc)
|
||||
|
||||
time_tz_specific = (dtm.datetime.now(
|
||||
tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))
|
||||
) + dtm.timedelta(seconds=2)).timetz()
|
||||
time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
|
||||
job_daily1 = job_queue.run_daily(self.job_run_once, time_tz_specific)
|
||||
job_daily2 = job_queue.run_daily(self.job_run_once, time_tz_utc)
|
||||
|
||||
assert job_once1.tzinfo == when_dt_tz_specific.tzinfo
|
||||
assert job_once2.tzinfo == _UTC
|
||||
assert job_once3.tzinfo == when_time_tz_specific.tzinfo
|
||||
assert job_once4.tzinfo == _UTC
|
||||
assert job_repeating1.tzinfo == first_dt_tz_specific.tzinfo
|
||||
assert job_repeating2.tzinfo == _UTC
|
||||
assert job_repeating3.tzinfo == first_time_tz_specific.tzinfo
|
||||
assert job_repeating4.tzinfo == _UTC
|
||||
assert job_daily1.tzinfo == time_tz_specific.tzinfo
|
||||
assert job_daily2.tzinfo == _UTC
|
||||
|
|
Loading…
Reference in a new issue