8000 Refactor JobQueue (#1981) · Konano/python-telegram-bot@19a4f9e · GitHub
[go: up one dir, main page]

Skip to content

Commit 19a4f9e

Browse files
committed
Refactor JobQueue (python-telegram-bot#1981)
* First go on refactoring JobQueue * Temporarily enable tests for the v13 branch * Work on tests * Temporarily enable tests for the v13 branch * Increase coverage * Remove JobQueue.tick() * Address review * Temporarily enable tests for the v13 branch * Address review * Dispatch errors * Fix handling of job_kwargs * Remove possibility to pass a Bot to JobQueue
1 parent 3930072 commit 19a4f9e

File tree

12 files changed

+544
-725
lines changed

12 files changed

+544
-725
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ${{matrix.os}}
1616
strategy:
1717
matrix:
18-
python-version: [3.5, 3.6, 3.7, 3.8]
18+
python-version: [3.6, 3.7, 3.8]
1919
os: [ubuntu-latest, windows-latest]
2020
include:
2121
- os: ubuntu-latest

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Introduction
8383

8484
This library provides a pure Python interface for the
8585
`Telegram Bot API <https://core.telegram.org/bots/api>`_.
86-
It's compatible with Python versions 3.5+. PTB might also work on `PyPy <http://pypy.org/>`_, though there have been a lot of issues before. Hence, PyPy is not officially supported.
86+
It's compatible with Python versions 3.6+. PTB might also work on `PyPy <http://pypy.org/>`_, though there have been a lot of issues before. Hence, PyPy is not officially supported.
8787

8888
In addition to the pure API implementation, this library features a number of high-level classes to
8989
make the development of bots easy and straightforward. These classes are contained in the

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ certifi
22
tornado>=5.1
33
cryptography
44
decorator>=4.4.0
5+
APScheduler==3.6.3

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ def requirements():
6161
'Topic :: Internet',
6262
'Programming Language :: Python',
6363
'Programming Language :: Python :: 3',
64-
'Programming Language :: Python :: 3.5',
6564
'Programming Language :: Python :: 3.6',
6665
'Programming Language :: Python :: 3.7',
6766
'Programming Language :: Python :: 3.8',

telegram/ext/jobqueue.py

Lines changed: 271 additions & 411 deletions
Large diffs are not rendered by default.

tests/conftest.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from time import sleep
2626

2727
import pytest
28+
import pytz
2829

2930
from telegram import (Bot, Message, User, Chat, MessageEntity, Update,
3031
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery,
@@ -271,14 +272,14 @@ def false_update(request):
271272
return Update(update_id=1, **request.param)
272273

273274

274-
@pytest.fixture(params=[1, 2], ids=lambda h: 'UTC +{hour:0>2}:00'.format(hour=h))
275-
def utc_offset(request):
276-
return datetime.timedelta(hours=request.param)
275+
@pytest.fixture(params=['Europe/Berlin', 'Asia/Singapore', 'UTC'])
276+
def tzinfo(request):
277+
return pytz.timezone(request.param)
277278

278279

279280
@pytest.fixture()
280-
def timezone(utc_offset):
281-
return datetime.timezone(utc_offset)
281+
def timezone(tzinfo):
282+
return tzinfo
282283

283284

284285
def expect_bad_request(func, message, reason):

tests/test_conversationhandler.py

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
PreCheckoutQuery, ShippingQuery, Update, User, MessageEntity)
2626
from telegram.ext import (ConversationHandler, CommandHandler, CallbackQueryHandler,
2727
MessageHandler, Filters, InlineQueryHandler, CallbackContext,
28-
DispatcherHandlerStop, TypeHandler)
28+
DispatcherHandlerStop, TypeHandler, JobQueue)
2929

3030

3131
@pytest.fixture(scope='class')
@@ -38,6 +38,15 @@ def user2():
3838
return User(first_name='Mister Test', id=124, is_bot=False)
3939

4040

41+
@pytest.fixture(autouse=True)
42+
def start_stop_job_queue(dp):
43+
dp.job_queue = JobQueue()
44+
dp.job_queue.set_dispatcher(dp)
45+
dp.job_queue.start()
46+
yield
47+
dp.job_queue.stop()
48+
49+
4150
def raise_dphs(func):
4251
def decorator(self, *args, **kwargs):
4352
result = func(self, *args, **kwargs)
@@ -610,20 +619,17 @@ def test_conversation_timeout(self, dp, bot, user1):
610619
bot=bot)
611620
dp.process_update(Update(update_id=0, message=message))
612621
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
613-
sleep(0.5)
614-
dp.job_queue.tick()
622+
sleep(0.65)
615623
assert handler.conversations.get((self.group.id, user1.id)) is None
616624

617625
# Start state machine, do something, then reach timeout
618626
dp.process_update(Update(update_id=1, message=message))
619627
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
620628
message.text = '/brew'
621629
message.entities[0].length = len('/brew')
622-
dp.job_queue.tick()
623630
dp.process_update(Update(update_id=2, message=message))
624631
assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING
625-
sleep(0.5)
626-
dp.job_queue.tick()
632+
sleep(0.6)
627633
assert handler.conversations.get((self.group.id, user1.id)) is None
628634

629635
def test_conversation_timeout_dispatcher_handler_stop(self, dp, bot, user1, caplog):
@@ -645,8 +651,7 @@ def timeout(*args, **kwargs):
645651
with caplog.at_level(logging.WARNING):
646652
dp.process_update(Update(update_id=0, message=message))
647653
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
648-
sleep(0.5)
649-
dp.job_queue.tick()
654+
sleep(0.8)
650655
assert handler.conversations.get((self.group.id, user1.id)) is None
651656
assert len(caplog.records) == 1
652657
rec = caplog.records[-1]
@@ -684,8 +689,7 @@ def timeout_callback(u, c):
684689
timeout_handler.callback = timeout_callback
685690

686691
cdp.process_update(update)
687-
sleep(0.5)
688-
cdp.job_queue.tick()
692+
sleep(0.6)
689693
assert handler.conversations.get((self.group.id, user1.id)) is None
690694
assert self.is_timeout
691695

@@ -708,24 +712,20 @@ def test_conversation_timeout_keeps_extending(self, dp, bot, user1):
708712
dp.process_update(Update(update_id=0, message=message))
709713
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
710714
sleep(0.25) # t=.25
711-
dp.job_queue.tick()
712715
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
713716
message.text = '/brew'
714717
message.entities[0].length = len('/brew')
715718
dp.process_update(Update(update_id=0, message=message))
716719
assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING
717720
sleep(0.35) # t=.6
718-
dp.job_queue.tick()
719721
assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING
720722
message.text = '/pourCoffee'
721723
message.entities[0].length = len('/pourCoffee')
722724
dp.process_update(Update(update_id=0, message=message))
723725
assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING
724726
sleep(.4) # t=1
725-
dp.job_queue.tick()
726727
assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING
727-
sleep(.1) # t=1.1
728-
dp.job_queue.tick()
728+
sleep(.2) # t=1.2
729729
assert handler.conversations.get((self.group.id, user1.id)) is None
730730

731731
def test_conversation_timeout_two_users(self, dp, bot, user1, user2):
@@ -744,16 +744,13 @@ def test_conversation_timeout_two_users(self, dp, bot, user1, user2):
744744
message.entities[0].length = len('/brew')
745745
message.entities[0].length = len('/brew')
746746
message.from_user = user2
747-
dp.job_queue.tick()
748747
dp.process_update(Update(update_id=0, message=message))
749748
assert handler.conversations.get((self.group.id, user2.id)) is None
750749
message.text = '/start'
751750
message.entities[0].length = len('/start')
752-
dp.job_queue.tick()
753751
dp.process_update(Update(update_id=0, message=message))
754752
assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY
755-
sleep(0.5)
756-
dp.job_queue.tick()
753+
sleep(0.6)
757754
assert handler.conversations.get((self.group.id, user1.id)) is None
758755
assert handler.conversations.get((self.group.id, user2.id)) is None
759756

@@ -776,8 +773,7 @@ def test_conversation_handler_timeout_state(self, dp, bot, user1):
776773
message. 1CF5 text = '/brew'
777774
message.entities[0].length = len('/brew')
778775
dp.process_update(Update(update_id=0, message=message))
779-
sleep(0.5)
780-
dp.job_queue.tick()
776+ F438
sleep(0.6)
781777
assert handler.conversations.get((self.group.id, user1.id)) is None
782778
assert self.is_timeout
783779

@@ -786,8 +782,7 @@ def test_conversation_handler_timeout_state(self, dp, bot, user1):
786782
message.text = '/start'
787783
message.entities[0].length = len('/start')
788784
dp.process_update(Update(update_id=1, message=message))
789-
sleep(0.5)
790-
dp.job_queue.tick()
785+
sleep(0.6)
791786
assert handler.conversations.get((self.group.id, user1.id)) is None
792787
assert self.is_timeout
793788

@@ -800,8 +795,7 @@ def test_conversation_handler_timeout_state(self, dp, bot, user1):
800795
message.text = '/startCoding'
801796
message.entities[0].length = len('/startCoding')
802797
dp.process_update(Update(update_id=0, message=message))
803-
sleep(0.5)
804-
dp.job_queue.tick()
798+
sleep(0.6)
805799
assert handler.conversations.get((self.group.id, user1.id)) is None
806800
assert not self.is_timeout
807801

@@ -824,8 +818,7 @@ def test_conversation_handler_timeout_state_context(self, cdp, bot, user1):
824818
message.text = '/brew'
825819
message.entities[0].length = len('/brew')
826820
cdp.process_update(Update(update_id=0, message=message))
827-
sleep(0.5)
828-
cdp.job_queue.tick()
821+
sleep(0.6)
829822
assert handler.conversations.get((self.group.id, user1.id)) is None
830823
assert self.is_timeout
831824

@@ -834,8 +827,7 @@ def test_conversation_handler_timeout_state_context(self, cdp, bot, user1):
834827
message.text = '/start'
835828
message.entities[0].length = len('/start')
836829
cdp.process_update(Update(update_id=1, message=message))
837-
sleep(0.5)
838-
cdp.job_queue.tick()
830+
sleep(0.6)
839831
assert handler.conversations.get((self.group.id, user1.id)) is None
840832
assert self.is_timeout
841833

@@ -848,8 +840,7 @@ def test_conversation_handler_timeout_state_context(self, cdp, bot, user1):
848840
message.text = '/startCoding'
849841
message.entities[0].length = len('/startCoding')
850842
cdp.process_update(Update(update_id=0, message=message))
851-
sleep(0.5)
852-
cdp.job_queue.tick()
843+
sleep(0.6)
853844
assert handler.conversations.get((self.group.id, user1.id)) is None
854845
assert not self.is_timeout
855846

@@ -865,7 +856,6 @@ def test_conversation_timeout_cancel_conflict(self, dp, bot, user1):
865856
def slowbrew(_bot, update):
866857
sleep(0.25)
867858
# Let's give to the original timeout a chance to execute
868-
dp.job_queue.tick()
869859
sleep(0.25)
870860
# By returning None we do not override the conversation state so
871861
# we can see if the timeout has been executed
@@ -887,16 +877,13 @@ def slowbrew(_bot, update):
887877
bot=bot)
888878
dp.process_update(Update(update_id=0, message=message))
889879
sleep(0.25)
890-
dp.job_queue.tick()
891880
message.text = '/slowbrew'
892881
message.entities[0].length = len('/slowbrew')
893882
dp.process_update(Update(update_id=0, message=message))
894-
dp.job_queue.tick()
895883
assert handler.conversations.get((self.group.id, user1.id)) is not None
896884
assert not self.is_timeout
897885

898-
sleep(0.5)
899-
dp.job_queue.tick()
886+
sleep(0.6)
900887
assert handler.conversations.get((self.group.id, user1.id)) is None
901888
assert self.is_timeout
902889

tests/test_helpers.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ def test_to_float_timestamp_absolute_aware(self, timezone):
8686
"""Conversion from timezone-aware datetime to timestamp"""
8787
# we're parametrizing this with two different UTC offsets to exclude the possibility
8888
# of an xpass when the test is run in a timezone with the same UTC offset
89-
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5, tzinfo=timezone)
89+
test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
90+
datetime = timezone.localize(test_datetime)
9091
assert (helpers.to_float_timestamp(datetime)
91-
== 1573431976.1 - timezone.utcoffset(None).total_seconds())
92+
== 1573431976.1 - timezone.utcoffset(test_datetime).total_seconds())
9293

9394
def test_to_float_timestamp_absolute_no_reference(self):
9495
"""A reference timestamp is only relevant for relative time specifications"""
@@ -116,14 +117,15 @@ def test_to_float_timestamp_time_of_day_timezone(self, timezone):
116117
"""Conversion from timezone-aware time-of-day specification to timestamp"""
117118
# we're parametrizing this with two different UTC offsets to exclude the possibility
118119
# of an xpass when the test is run in a timezone with the same UTC offset
119-
utc_offset = timezone.utcoffset(None)
120120
ref_datetime = dtm.datetime(1970, 1, 1, 12)
121+
utc_offset = timezone.utcoffset(ref_datetime)
121122
ref_t, time_of_day = _datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
123+
aware_time_of_day = timezone.localize(ref_datetime).timetz()
122124

123125
# first test that naive time is assumed to be utc:
124126
assert helpers.to_float_timestamp(time_of_day, ref_t) == pytest.approx(ref_t)
125127
# test that by setting the timezone the timestamp changes accordingly:
126-
assert (helpers.to_float_timestamp(time_of_day.replace(tzinfo=timezone), ref_t)
128+
assert (helpers.to_float_timestamp(aware_time_of_day, ref_t)
127129
== pytest.approx(ref_t + (-utc_offset.total_seconds() % (24 * 60 * 60))))
128130

129131
@pytest.mark.parametrize('time_spec', RELATIVE_TIME_SPECS, ids=str)
@@ -149,9 +151,10 @@ def test_from_timestamp_naive(self):
149151
def test_from_timestamp_aware(self, timezone):
150152
# we're parametrizing this with two different UTC offsets to exclude the possibility
151153
# of an xpass when the test is run in a timezone with the same UTC offset
152-
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5, tzinfo=timezone)
153-
assert (helpers.from_timestamp(1573431976.1 - timezone.utcoffset(None).total_seconds())
154-
== datetime)
154+
test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10 ** 5)
155+
datetime = timezone.localize(test_datetime)
156+
assert (helpers.from_timestamp(
157+
1573431976.1 - timezone.utcoffset(test_datetime).total_seconds()) == datetime)
155158

156159
def test_create_deep_linked_url(self):
157160
username = 'JamesTheMock'
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ def test_subprocess_pipe(self):
5151
def test_mimetypes(self):
5252
# Only test a few to make sure logic works okay
5353
assert InputFile(open('tests/data/telegram.jpg', 'rb')).mimetype == 'image/jpeg'
54-
if sys.version_info >= (3, 5):
55-
assert InputFile(open('tests/data/telegram.webp', 'rb')).mimetype == 'image/webp'
54+
assert InputFile(open('tests/data/telegram.webp', 'rb')).mimetype == 'image/webp'
5655
assert InputFile(open('tests/data/telegram.mp3', 'rb')).mimetype == 'audio/mpeg'
5756

5857
# Test guess from file
0