diff --git a/docs/source/examples.rst b/docs/source/examples.rst index c39a4785dd8..3c949eb88a4 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -40,6 +40,7 @@ up a job to send a message to that user after 30 seconds. The user can also cancel the timer by sending ``/unset``. To learn more about the ``JobQueue``, read `this wiki article `__. +Note: To use ``JobQueue``, you must install PTB via ``pip install python-telegram-bot[job-queue]`` :any:`examples.conversationbot` ------------------------------- @@ -115,6 +116,7 @@ Don’t forget to enable and configure payments with `@BotFather `_. Check out this `guide `__ on Telegram passports in PTB. +Note: To use Telegram Passport, you must install PTB via ``pip install python-telegram-bot[passport]`` :any:`examples.paymentbot` -------------------------- @@ -162,6 +164,7 @@ combination with ``telegram.ext.Application``. This example showcases how PTBs “arbitrary callback data” feature can be used. +Note: To use arbitrary callback data, you must install PTB via ``pip install python-telegram-bot[callback-data]`` Pure API -------- diff --git a/examples/arbitrarycallbackdatabot.py b/examples/arbitrarycallbackdatabot.py index 0b0ffa1d71a..3fca2c5fee7 100644 --- a/examples/arbitrarycallbackdatabot.py +++ b/examples/arbitrarycallbackdatabot.py @@ -6,6 +6,10 @@ For detailed info on arbitrary callback data, see the wiki page at https://github.com/python-telegram-bot/python-telegram-bot/wiki/Arbitrary-callback_data + +Note: +To use arbitrary callback data, you must install PTB via +`pip install python-telegram-bot[callback-data]` """ import logging from typing import List, Tuple, cast diff --git a/examples/passportbot.py b/examples/passportbot.py index 3cb03ef2384..77281e85e22 100644 --- a/examples/passportbot.py +++ b/examples/passportbot.py @@ -10,6 +10,9 @@ See https://github.com/python-telegram-bot/python-telegram-bot/wiki/Telegram-Passport for how to use Telegram Passport properly with python-telegram-bot. +Note: +To use Telegram Passport, you must install PTB via +`pip install python-telegram-bot[passport]` """ import logging from pathlib import Path diff --git a/examples/timerbot.py b/examples/timerbot.py index 02d2af7fe76..29f45caaf5c 100644 --- a/examples/timerbot.py +++ b/examples/timerbot.py @@ -16,6 +16,10 @@ Basic Alarm Bot example, sends a message after a set time. Press Ctrl-C on the command line or send a signal to the process to stop the bot. + +Note: +To use arbitrary callback data, you must install ptb via +`pip install python-telegram-bot[callback-data]` """ import logging diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index 5543fafc5c7..0311787b07b 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -64,7 +64,7 @@ if TYPE_CHECKING: from telegram import Message - from telegram.ext import ConversationHandler + from telegram.ext import ConversationHandler, JobQueue from telegram.ext._applicationbuilder import InitApplicationBuilder from telegram.ext._jobqueue import Job @@ -151,8 +151,6 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager) update_queue (:class:`asyncio.Queue`): The synchronized queue that will contain the updates. updater (:class:`telegram.ext.Updater`): Optional. The updater used by this application. - job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue` - instance to pass onto handler callbacks. chat_data (:obj:`types.MappingProxyType`): A dictionary handlers can use to store data for the chat. For each integer chat id, the corresponding value of this mapping is available as :attr:`telegram.ext.CallbackContext.chat_data` in handler callbacks for @@ -218,6 +216,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager) "_concurrent_updates_sem", "_conversation_handler_conversations", "_initialized", + "_job_queue", "_running", "_user_data", "_user_ids_to_be_deleted_in_persistence", @@ -228,7 +227,6 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager) "context_types", "error_handlers", "handlers", - "job_queue", "persistence", "post_init", "post_shutdown", @@ -264,7 +262,6 @@ def __init__( self.bot = bot self.update_queue = update_queue - self.job_queue = job_queue self.context_types = context_types self.updater = updater self.handlers: Dict[int, List[BaseHandler]] = {} @@ -306,6 +303,7 @@ def __init__( # A number of low-level helpers for the internal logic self._initialized = False self._running = False + self._job_queue = job_queue self.__update_fetcher_task: Optional[asyncio.Task] = None self.__update_persistence_task: Optional[asyncio.Task] = None self.__update_persistence_event = asyncio.Event() @@ -337,6 +335,23 @@ def concurrent_updates(self) -> int: """ return self._concurrent_updates + @property + def job_queue(self) -> Optional["JobQueue"]: + """ + :class:`telegram.ext.JobQueue`: The :class:`JobQueue` used by the + :class:`telegram.ext.Application`. + + .. seealso:: `Job Queue `_ + """ + if self._job_queue is None: + warn( + "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " + "`pip install python-telegram-bot[job_queue]`.", + stacklevel=2, + ) + return self._job_queue + async def initialize(self) -> None: """Initializes the Application by initializing: @@ -511,8 +526,8 @@ async def start(self) -> None: ) _logger.debug("Loop for updating persistence started") - if self.job_queue: - await self.job_queue.start() # type: ignore[union-attr] + if self._job_queue: + await self._job_queue.start() # type: ignore[union-attr] _logger.debug("JobQueue started") self.__update_fetcher_task = asyncio.create_task( @@ -561,9 +576,9 @@ async def stop(self) -> None: await self.__update_fetcher_task _logger.debug("Application stopped fetching of updates.") - if self.job_queue: + if self._job_queue: _logger.debug("Waiting for running jobs to finish") - await self.job_queue.stop(wait=True) # type: ignore[union-attr] + await self._job_queue.stop(wait=True) # type: ignore[union-attr] _logger.debug("JobQueue stopped") _logger.debug("Waiting for `create_task` calls to be processed") diff --git a/telegram/ext/_callbackcontext.py b/telegram/ext/_callbackcontext.py index e7b99b4c173..eaa99f03197 100644 --- a/telegram/ext/_callbackcontext.py +++ b/telegram/ext/_callbackcontext.py @@ -21,6 +21,7 @@ from telegram._callbackquery import CallbackQuery from telegram._update import Update +from telegram._utils.warnings import warn from telegram.ext._extbot import ExtBot from telegram.ext._utils.types import BD, BT, CD, UD @@ -389,7 +390,13 @@ def job_queue(self) -> Optional["JobQueue"]: .. seealso:: `Job Queue `_ """ - return self._application.job_queue + if self._application._job_queue is None: # pylint: disable=protected-access + warn( + "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " + "`pip install python-telegram-bot[job_queue]`.", + stacklevel=2, + ) + return self._application._job_queue # pylint: disable=protected-access @property def update_queue(self) -> "Queue[object]": diff --git a/telegram/ext/_conversationhandler.py b/telegram/ext/_conversationhandler.py index 0402f740a7c..277bbf8a254 100644 --- a/telegram/ext/_conversationhandler.py +++ b/telegram/ext/_conversationhandler.py @@ -676,7 +676,7 @@ def _schedule_job( try: # both job_queue & conversation_timeout are checked before calling _schedule_job j_queue = application.job_queue - self.timeout_jobs[conversation_key] = j_queue.run_once( + self.timeout_jobs[conversation_key] = j_queue.run_once( # type: ignore[union-attr] self._trigger_timeout, self.conversation_timeout, # type: ignore[arg-type] data=_ConversationTimeoutContext(conversation_key, update, application, context), diff --git a/tests/test_application.py b/tests/test_application.py index eb5b741b116..35dedb7fbec 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -201,6 +201,18 @@ async def post_shutdown(application: Application) -> None: post_shutdown=None, ) + def test_job_queue(self, bot, app, recwarn): + expected_warning = ( + "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " + "`pip install python-telegram-bot[job_queue]`." + ) + assert app.job_queue is app._job_queue + application = ApplicationBuilder().token(bot.token).job_queue(None).build() + assert application.job_queue is None + assert len(recwarn) == 1 + assert str(recwarn[0].message) == expected_warning + assert recwarn[0].filename == __file__, "wrong stacklevel" + def test_custom_context_init(self, bot): cc = ContextTypes( context=CustomContext, @@ -385,6 +397,7 @@ def test_builder(self, app): builder_2.token(app.bot.token) @pytest.mark.parametrize("job_queue", (True, False)) + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") async def test_start_stop_processing_updates(self, bot, job_queue): # TODO: repeat a similar test for create_task, persistence processing and job queue if job_queue: diff --git a/tests/test_applicationbuilder.py b/tests/test_applicationbuilder.py index 6f196f2f3dc..da6be21a98b 100644 --- a/tests/test_applicationbuilder.py +++ b/tests/test_applicationbuilder.py @@ -50,6 +50,7 @@ def builder(): @pytest.mark.skipif(TEST_WITH_OPT_DEPS, reason="Optional dependencies are installed") class TestApplicationBuilderNoOptDeps: + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") def test_init(self, builder): builder.token("token") app = builder.build() @@ -416,6 +417,7 @@ def test_no_updater(self, bot, builder): assert isinstance(app.job_queue, JobQueue) assert app.job_queue.application is app + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") def test_no_job_queue(self, bot, builder): app = builder.token(bot.token).job_queue(None).build() assert app.bot.token == bot.token diff --git a/tests/test_callbackcontext.py b/tests/test_callbackcontext.py index 3f98aa65544..007cf143896 100644 --- a/tests/test_callbackcontext.py +++ b/tests/test_callbackcontext.py @@ -38,7 +38,7 @@ class TestCallbackContext: - def test_slot_behaviour(self, app, mro_slots, recwarn): + def test_slot_behaviour(self, app, mro_slots): c = CallbackContext(app) for attr in c.__slots__: assert getattr(c, attr, "err") != "err", f"got extra slot '{attr}'" @@ -58,6 +58,21 @@ def test_from_job(self, app): assert callback_context.job_queue is app.job_queue assert callback_context.update_queue is app.update_queue + def test_job_queue(self, bot, app, recwarn): + expected_warning = ( + "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " + "`pip install python-telegram-bot[job_queue]`." + ) + + callback_context = CallbackContext(app) + assert callback_context.job_queue is app.job_queue + app = ApplicationBuilder().job_queue(None).token(bot.token).build() + callback_context = CallbackContext(app) + assert callback_context.job_queue is None + assert len(recwarn) == 1 + assert str(recwarn[0].message) == expected_warning + assert recwarn[0].filename == __file__, "wrong stacklevel" + def test_from_update(self, app): update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 8fa461df651..fcb00889129 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -996,11 +996,8 @@ async def test_no_running_job_queue_warning(self, app, bot, user1, recwarn, jq): fallbacks=self.fallbacks, conversation_timeout=0.5, ) - # save app.job_queue in temp variable jqueue - # and then set app.job_queue to None. - jqueue = app.job_queue if not jq: - app.job_queue = None + app = ApplicationBuilder().token(bot.token).job_queue(None).build() app.add_handler(handler) message = Message( @@ -1018,11 +1015,15 @@ async def test_no_running_job_queue_warning(self, app, bot, user1, recwarn, jq): async with app: await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.5) - assert len(recwarn) == 1 - assert str(recwarn[0].message).startswith("Ignoring `conversation_timeout`") - assert ("is not running" if jq else "has no JobQueue.") in str(recwarn[0].message) + if jq: + assert len(recwarn) == 1 + else: + assert len(recwarn) == 2 + assert str(recwarn[0].message if jq else recwarn[1].message).startswith( + "Ignoring `conversation_timeout`" + ) + assert ("is not running" if jq else "No `JobQueue` set up.") in str(recwarn[0].message) # now set app.job_queue back to it's original value - app.job_queue = jqueue async def test_schedule_job_exception(self, app, bot, user1, monkeypatch, caplog): def mocked_run_once(*a, **kw): @@ -1031,7 +1032,7 @@ def mocked_run_once(*a, **kw): class DictJB(JobQueue): pass - app.job_queue = DictJB() + app = ApplicationBuilder().token(bot.token).job_queue(DictJB()).build() monkeypatch.setattr(app.job_queue, "run_once", mocked_run_once) handler = ConversationHandler( entry_points=self.entry_points,