From c4393ed84cc04a98c6994901c9833e66bfcf6a59 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 1 Jan 2020 21:55:34 +0300 Subject: [PATCH 01/10] 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 --- AUTHORS.rst | 1 + telegram/ext/jobqueue.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 63cd5e0bdb5..3ae4a34633c 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,6 +18,7 @@ The following wonderful people contributed directly or indirectly to this projec - `Alateas `_ - `Ales Dokshanin `_ - `Ambro17 `_ +- `Andrej Zhilenkov `_ - `Anton Tagunov `_ - `Avanatiker `_ - `Balduro `_ diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 5a04d4ea5d2..ad9aa158b05 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -103,6 +103,7 @@ def _put(self, job, time_spec=None, previous_t=None): # enqueue: self.logger.debug('Putting job %s with t=%f', job.name, time_spec) self._queue.put((next_t, job)) + job._next_t = next_t # Wake up the loop if this job should be executed next self._set_next_peek(next_t) @@ -376,7 +377,8 @@ class Job(object): Only optional for backward compatibility with ``JobQueue.put()``. tzinfo (:obj:`datetime.tzinfo`, optional): timezone associated to this job. Used when checking the day of the week to determine whether a job should run (only relevant when - ``days is not Days.EVERY_DAY``). Defaults to UTC. + ``days is not Days.EVERY_DAY``); also used when :attr:`next_t` is retrieved. + Defaults to UTC. """ def __init__(self, @@ -396,6 +398,7 @@ def __init__(self, self._repeat = None self._interval = None self.interval = interval + self._next_t = None self.repeat = repeat self._days = None @@ -422,6 +425,7 @@ def schedule_removal(self): """ self._remove.set() + self._next_t = None @property def removed(self): @@ -469,6 +473,17 @@ def interval_seconds(self): 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 it equals to ``None``. + + """ + return datetime.datetime.fromtimestamp(self._next_t, self.tzinfo) if self._next_t else None + + @property def repeat(self): """:obj:`bool`: Optional. If this job should periodically execute its callback function.""" From 176ff23d7fd1873c3340b99cd7e4fd08c0902ae0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 1 Jan 2020 22:56:33 +0300 Subject: [PATCH 02/10] Fixed newline and trailing whitespace --- telegram/ext/jobqueue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index ad9aa158b05..9db7343abb5 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -377,7 +377,7 @@ class Job(object): Only optional for backward compatibility with ``JobQueue.put()``. tzinfo (:obj:`datetime.tzinfo`, optional): timezone associated to this job. Used when checking the day of the week to determine whether a job should run (only relevant when - ``days is not Days.EVERY_DAY``); also used when :attr:`next_t` is retrieved. + ``days is not Days.EVERY_DAY``); also used when :attr:`next_t` is retrieved. Defaults to UTC. """ @@ -483,7 +483,6 @@ def next_t(self): """ return datetime.datetime.fromtimestamp(self._next_t, self.tzinfo) if self._next_t else None - @property def repeat(self): """:obj:`bool`: Optional. If this job should periodically execute its callback function.""" From 7b30ee3553efb32336b2b6ffa29a8f6fda1b2935 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 2 Jan 2020 12:09:53 +0300 Subject: [PATCH 03/10] 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. --- telegram/ext/jobqueue.py | 12 ++++++++---- tests/test_jobqueue.py | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 9db7343abb5..96ac66daee9 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -103,7 +103,7 @@ def _put(self, job, time_spec=None, previous_t=None): # enqueue: self.logger.debug('Putting job %s with t=%f', job.name, time_spec) self._queue.put((next_t, job)) - job._next_t = next_t + job.next_t = next_t # Wake up the loop if this job should be executed next self._set_next_peek(next_t) @@ -289,6 +289,7 @@ def tick(self): if job.repeat and not job.removed: self._put(job, previous_t=t) else: + job.next_t = None self.logger.debug('Dropping non-repeating or removed job %s', job.name) def start(self): @@ -377,8 +378,7 @@ class Job(object): Only optional for backward compatibility with ``JobQueue.put()``. tzinfo (:obj:`datetime.tzinfo`, optional): timezone associated to this job. Used when checking the day of the week to determine whether a job should run (only relevant when - ``days is not Days.EVERY_DAY``); also used when :attr:`next_t` is retrieved. - Defaults to UTC. + ``days is not Days.EVERY_DAY``). Defaults to UTC. """ def __init__(self, @@ -478,11 +478,15 @@ def next_t(self): """ ::obj:`datetime.datetime`: Datetime for the next job execution. Datetime is localized according to :attr:`tzinfo`. - If job is removed it equals to ``None``. + 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 + @next_t.setter + def next_t(self, next_t): + self._next_t = next_t + @property def repeat(self): """:obj:`bool`: Optional. If this job should periodically execute its callback function.""" diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 4b5d8d10852..394cd40ccfa 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -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 @@ -325,3 +326,29 @@ def test_context_based_callback(self, job_queue): sleep(0.03) assert self.result == 0 + + 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.05, name='run_once job') + job2 = job_queue.run_once(self.job_run_once, 0.05, name='canceled run_once job') + job3 = job_queue.run_repeating(self.job_run_once, 0.02, name='repeatable job') + + sleep(0.03) + 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 == None + else: + assert job.next_t == t + + sleep(0.03) + + assert job1.next_t == None + assert job2.next_t == None From b7cafd1bf50139d4bfc0540d580ca107415ac9b9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 2 Jan 2020 12:17:56 +0300 Subject: [PATCH 04/10] Fixed Flake8 issues --- telegram/ext/jobqueue.py | 2 +- tests/test_jobqueue.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 96ac66daee9..cafcc9da11b 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -485,7 +485,7 @@ def next_t(self): @next_t.setter def next_t(self, next_t): - self._next_t = next_t + self._next_t = next_t @property def repeat(self): diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 394cd40ccfa..abfd4861fd3 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -334,7 +334,7 @@ def test_job_next_t_property(self, job_queue): job1 = job_queue.run_once(self.job_run_once, 0.05, name='run_once job') job2 = job_queue.run_once(self.job_run_once, 0.05, name='canceled run_once job') - job3 = job_queue.run_repeating(self.job_run_once, 0.02, name='repeatable job') + job_queue.run_repeating(self.job_run_once, 0.02, name='repeatable job') sleep(0.03) job2.schedule_removal() @@ -344,11 +344,11 @@ def test_job_next_t_property(self, job_queue): t = dtm.datetime.fromtimestamp(t, job.tzinfo) if job.removed: - assert job.next_t == None + assert job.next_t is None else: assert job.next_t == t sleep(0.03) - assert job1.next_t == None - assert job2.next_t == None + assert job1.next_t is None + assert job2.next_t is None From 6b1fce523d87aca9aee4125b67527828ba36f392 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 3 Jan 2020 11:07:09 +0300 Subject: [PATCH 05/10] 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. --- telegram/ext/jobqueue.py | 12 ++++++++++-- tests/test_jobqueue.py | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index cafcc9da11b..6188f4df060 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -485,6 +485,14 @@ def next_t(self): @next_t.setter def 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.replace(tzinfo=datetime.timezone.utc) + next_t = to_float_timestamp(next_t) + elif not (isinstance(next_t, float) or next_t is None): + raise TypeError("The 'next_t' argument should be of type 'float' " + "or 'datetime.datetime' or 'NoneType'") + self._next_t = next_t @property @@ -506,10 +514,10 @@ def days(self): @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 " diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index abfd4861fd3..3a229e395c2 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -331,12 +331,13 @@ 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 + # - next_t setter for 'datetime.datetime' values - job1 = job_queue.run_once(self.job_run_once, 0.05, name='run_once job') - job2 = job_queue.run_once(self.job_run_once, 0.05, name='canceled run_once job') - job_queue.run_repeating(self.job_run_once, 0.02, name='repeatable job') + 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.03) + sleep(0.045) job2.schedule_removal() with job_queue._queue.mutex: @@ -348,7 +349,14 @@ def test_job_next_t_property(self, job_queue): else: assert job.next_t == t - sleep(0.03) + assert self.result == 1 + sleep(0.02) + assert self.result == 2 assert job1.next_t is None assert job2.next_t is None + + t = dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=12))) + job1.next_t = t + job1.tzinfo = dtm.timezone.utc + assert job1.next_t == t.replace(tzinfo=job.tzinfo) From 8231df141ed2d9c24540dfe015ee2a8fa6c0aaa1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 3 Jan 2020 11:35:05 +0300 Subject: [PATCH 06/10] 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. --- telegram/ext/jobqueue.py | 10 +++++----- tests/test_jobqueue.py | 11 +++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 6188f4df060..98879f682cb 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -459,8 +459,8 @@ def interval(self, interval): 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 @@ -489,9 +489,9 @@ def next_t(self, next_t): # Set timezone to UTC in case datetime is in local timezone. next_t = next_t.replace(tzinfo=datetime.timezone.utc) next_t = to_float_timestamp(next_t) - elif not (isinstance(next_t, float) or next_t is None): - raise TypeError("The 'next_t' argument should be of type 'float' " - "or 'datetime.datetime' or 'NoneType'") + 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 diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 3a229e395c2..884f52aa608 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -294,18 +294,21 @@ def test_warnings(self, job_queue): 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.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') @@ -337,7 +340,7 @@ def test_job_next_t_property(self, job_queue): 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.045) + sleep(0.05) job2.schedule_removal() with job_queue._queue.mutex: From 8c04d69f664eda2bd58cf39a2f8458b1cfa7c300 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 3 Jan 2020 11:57:37 +0300 Subject: [PATCH 07/10] Python 2 compatibility for test_job_next_t_property Added _UTC and _UtcOffsetTimezone for python 2 compatibility --- telegram/ext/jobqueue.py | 2 +- tests/test_jobqueue.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 98879f682cb..2c35c7a9977 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -487,7 +487,7 @@ def next_t(self): def 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.replace(tzinfo=datetime.timezone.utc) + next_t = next_t.replace(tzinfo=_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: " diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 884f52aa608..05248b689c4 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -29,7 +29,7 @@ from telegram.ext import JobQueue, Updater, Job, CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning -from telegram.utils.helpers import _UtcOffsetTimezone +from telegram.utils.helpers import _UtcOffsetTimezone, _UTC @pytest.fixture(scope='function') @@ -359,7 +359,7 @@ def test_job_next_t_property(self, job_queue): assert job1.next_t is None assert job2.next_t is None - t = dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=12))) + t = dtm.datetime.now(tz=_UtcOffsetTimezone(dtm.timedelta(hours=12))) job1.next_t = t - job1.tzinfo = dtm.timezone.utc + job1.tzinfo = _UTC assert job1.next_t == t.replace(tzinfo=job.tzinfo) From 3c631123fe7c8dc25aaffd4176ee0c858860425f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 3 Jan 2020 14:30:17 +0300 Subject: [PATCH 08/10] 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. --- telegram/ext/jobqueue.py | 2 +- tests/test_jobqueue.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 2c35c7a9977..4517d920ea6 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -487,7 +487,7 @@ def next_t(self): def 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.replace(tzinfo=_UTC) + 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: " diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 05248b689c4..05afd72a917 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -29,7 +29,7 @@ from telegram.ext import JobQueue, Updater, Job, CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning -from telegram.utils.helpers import _UtcOffsetTimezone, _UTC +from telegram.utils.helpers import _UtcOffsetTimezone @pytest.fixture(scope='function') @@ -334,7 +334,6 @@ 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 - # - next_t setter for 'datetime.datetime' values 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') @@ -359,7 +358,12 @@ def test_job_next_t_property(self, job_queue): assert job1.next_t is None assert job2.next_t is None + def test_job_next_t_setter(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))) - job1.next_t = t - job1.tzinfo = _UTC - assert job1.next_t == t.replace(tzinfo=job.tzinfo) + job.next_t = t + job.tzinfo = _UtcOffsetTimezone(dtm.timedelta(hours=5)) + assert job.next_t == t.astimezone(job.tzinfo) From 051f4b2f185012847c624ceadd2cb42d0a9fac8f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Sat, 22 Feb 2020 11:40:09 +0300 Subject: [PATCH 09/10] 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. --- telegram/ext/jobqueue.py | 29 ++++++++++++++++----- tests/test_jobqueue.py | 56 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 4517d920ea6..5eeb0343ff3 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -131,6 +131,9 @@ def run_once(self, callback, when, context=None, name=None): job should run. This could be either today or, if the time has already passed, tomorrow. + If ``when`` is :obj:`datetime.datetime` or :obj:`datetime.datetime` 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 @@ -141,7 +144,14 @@ def run_once(self, callback, when, context=None, name=None): 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 @@ -171,6 +181,9 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None) job should run. This could be either today or, if the time has already passed, tomorrow. + If ``first`` is :obj:`datetime.datetime` or :obj:`datetime.datetime` 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``. @@ -187,12 +200,15 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None) 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 @@ -206,6 +222,7 @@ def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None 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`` also defines ``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. @@ -227,10 +244,10 @@ def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None interval=datetime.timedelta(days=1), repeat=True, days=days, - tzinfo=time.tzinfo, context=context, name=name, - job_queue=self) + job_queue=self, + tzinfo=time.tzinfo) self._put(job, time_spec=time) return job @@ -389,7 +406,7 @@ def __init__(self, days=Days.EVERY_DAY, name=None, job_queue=None, - tzinfo=_UTC): + tzinfo=None): self.callback = callback self.context = context @@ -403,7 +420,7 @@ def __init__(self, self._days = None self.days = days - self.tzinfo = tzinfo + self.tzinfo = tzinfo or _UTC self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 05afd72a917..8416d7bca34 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -29,7 +29,7 @@ from telegram.ext import JobQueue, Updater, Job, CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning -from telegram.utils.helpers import _UtcOffsetTimezone +from telegram.utils.helpers import _UtcOffsetTimezone, _UTC @pytest.fixture(scope='function') @@ -367,3 +367,57 @@ def test_job_next_t_setter(self, job_queue): job.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 From 895c6c2a17cb0facbc67d22d7d6a1a85f5ead54f Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 18 Apr 2020 14:52:37 +0200 Subject: [PATCH 10/10] address review --- telegram/ext/jobqueue.py | 17 ++++++++--------- tests/test_jobqueue.py | 6 +++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index e70cca4b697..03598db96a4 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -104,7 +104,7 @@ def _put(self, job, time_spec=None, previous_t=None): # enqueue: self.logger.debug('Putting job %s with t=%s', job.name, time_spec) self._queue.put((next_t, job)) - job.next_t = next_t + job._set_next_t(next_t) # Wake up the loop if this job should be executed next self._set_next_peek(next_t) @@ -136,7 +136,7 @@ def run_once(self, callback, when, context=None, name=None): 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.datetime` type + 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. @@ -190,7 +190,7 @@ def run_repeating(self, callback, interval, first=None, context=None, name=None) 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.datetime` type + 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`` @@ -234,7 +234,7 @@ def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None 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`` also defines ``Job.tzinfo``. + ``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. @@ -256,10 +256,10 @@ def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None interval=datetime.timedelta(days=1), repeat=True, days=days, + tzinfo=time.tzinfo, context=context, name=name, - job_queue=self, - tzinfo=time.tzinfo) + job_queue=self) self._put(job, time_spec=time) return job @@ -319,7 +319,7 @@ def tick(self): if job.repeat and not job.removed: self._put(job, previous_t=t) else: - job.next_t = None + job._set_next_t(None) self.logger.debug('Dropping non-repeating or removed job %s', job.name) def start(self): @@ -516,8 +516,7 @@ def next_t(self): """ return datetime.datetime.fromtimestamp(self._next_t, self.tzinfo) if self._next_t else None - @next_t.setter - def next_t(self, next_t): + 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) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 4219168080f..26ad75f7727 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -312,7 +312,7 @@ def test_warnings(self, job_queue): j.days = (0, 6, 12, 14) with pytest.raises(TypeError, match='argument should be one of the'): - j.next_t = 'tomorrow' + j._set_next_t('tomorrow') def test_get_jobs(self, job_queue): job1 = job_queue.run_once(self.job_run_once, 10, name='name1') @@ -374,13 +374,13 @@ def test_job_next_t_property(self, job_queue): assert job1.next_t is None assert job2.next_t is None - def test_job_next_t_setter(self, job_queue): + 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.next_t = t + job._set_next_t(t) job.tzinfo = _UtcOffsetTimezone(dtm.timedelta(hours=5)) assert job.next_t == t.astimezone(job.tzinfo)