Pass Failing Jobs to Error Handlers (#2692)

This commit is contained in:
Bibo-Joshi 2021-10-03 20:00:54 +02:00 committed by Hinrich Mahler
parent 51488bb4be
commit 90b82eed99
5 changed files with 72 additions and 52 deletions

View file

@ -6,3 +6,4 @@ telegram.ext.Job
.. autoclass:: telegram.ext.Job .. autoclass:: telegram.ext.Job
:members: :members:
:show-inheritance: :show-inheritance:
:special-members: __call__

View file

@ -86,7 +86,11 @@ class CallbackContext(Generic[UD, CD, BD]):
that raised the error. Only present when the raising function was run asynchronously that raised the error. Only present when the raising function was run asynchronously
using :meth:`telegram.ext.Dispatcher.run_async`. using :meth:`telegram.ext.Dispatcher.run_async`.
job (:class:`telegram.ext.Job`): Optional. The job which originated this callback. job (:class:`telegram.ext.Job`): Optional. The job which originated this callback.
Only present when passed to the callback of :class:`telegram.ext.Job`. Only present when passed to the callback of :class:`telegram.ext.Job` or in error
handlers if the error is caused by a job.
.. versionchanged:: 14.0
:attr:`job` is now also present in error handlers if the error is caused by a job.
""" """
@ -231,6 +235,7 @@ class CallbackContext(Generic[UD, CD, BD]):
dispatcher: 'Dispatcher[CCT, UD, CD, BD]', dispatcher: 'Dispatcher[CCT, UD, CD, BD]',
async_args: Union[List, Tuple] = None, async_args: Union[List, Tuple] = None,
async_kwargs: Dict[str, object] = None, async_kwargs: Dict[str, object] = None,
job: 'Job' = None,
) -> 'CCT': ) -> 'CCT':
""" """
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error
@ -244,12 +249,15 @@ class CallbackContext(Generic[UD, CD, BD]):
error (:obj:`Exception`): The error. error (:obj:`Exception`): The error.
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
context. context.
async_args (List[:obj:`object`]): Optional. Positional arguments of the function that async_args (List[:obj:`object`], optional): Positional arguments of the function that
raised the error. Pass only when the raising function was run asynchronously using raised the error. Pass only when the raising function was run asynchronously using
:meth:`telegram.ext.Dispatcher.run_async`. :meth:`telegram.ext.Dispatcher.run_async`.
async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the async_kwargs (Dict[:obj:`str`, :obj:`object`], optional): Keyword arguments of the
function that raised the error. Pass only when the raising function was run function that raised the error. Pass only when the raising function was run
asynchronously using :meth:`telegram.ext.Dispatcher.run_async`. asynchronously using :meth:`telegram.ext.Dispatcher.run_async`.
job (:class:`telegram.ext.Job`, optional): The job associated with the error.
.. versionadded:: 14.0
Returns: Returns:
:class:`telegram.ext.CallbackContext` :class:`telegram.ext.CallbackContext`
@ -258,6 +266,7 @@ class CallbackContext(Generic[UD, CD, BD]):
self.error = error self.error = error
self.async_args = async_args self.async_args = async_args
self.async_kwargs = async_kwargs self.async_kwargs = async_kwargs
self.job = job
return self return self
@classmethod @classmethod

View file

@ -52,8 +52,7 @@ from telegram.ext.utils.types import CCT, UD, CD, BD
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram import Bot from telegram import Bot
from telegram.ext import JobQueue from telegram.ext import JobQueue, Job, CallbackContext
from telegram.ext.callbackcontext import CallbackContext
DEFAULT_GROUP: int = 0 DEFAULT_GROUP: int = 0
@ -678,6 +677,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
update: Optional[object], update: Optional[object],
error: Exception, error: Exception,
promise: Promise = None, promise: Promise = None,
job: 'Job' = None,
) -> bool: ) -> bool:
"""Dispatches an error by passing it to all error handlers registered with """Dispatches an error by passing it to all error handlers registered with
:meth:`add_error_handler`. If one of the error handlers raises :meth:`add_error_handler`. If one of the error handlers raises
@ -696,6 +696,9 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
error (:obj:`Exception`): The error that was raised. error (:obj:`Exception`): The error that was raised.
promise (:class:`telegram.utils.Promise`, optional): The promise whose pooled function promise (:class:`telegram.utils.Promise`, optional): The promise whose pooled function
raised the error. raised the error.
job (:class:`telegram.ext.Job`, optional): The job that caused the error.
.. versionadded:: 14.0
Returns: Returns:
:obj:`bool`: :obj:`True` if one of the error handlers raised :obj:`bool`: :obj:`True` if one of the error handlers raised
@ -707,7 +710,12 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
if self.error_handlers: if self.error_handlers:
for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621 for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621
context = self.context_types.context.from_error( context = self.context_types.context.from_error(
update, error, self, async_args=async_args, async_kwargs=async_kwargs update=update,
error=error,
dispatcher=self,
async_args=async_args,
async_kwargs=async_kwargs,
job=job,
) )
if run_async: if run_async:
self.run_async(callback, update, context, update=update) self.run_async(callback, update, context, update=update)

View file

@ -19,20 +19,17 @@
"""This module contains the classes JobQueue and Job.""" """This module contains the classes JobQueue and Job."""
import datetime import datetime
import logging from typing import TYPE_CHECKING, Callable, Optional, Tuple, Union, cast, overload
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union, cast, overload
import pytz import pytz
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, JobEvent
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.job import Job as APSJob from apscheduler.job import Job as APSJob
from telegram.ext.callbackcontext import CallbackContext
from telegram.utils.types import JSONDict from telegram.utils.types import JSONDict
from .extbot import ExtBot from .extbot import ExtBot
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram.ext import Dispatcher from telegram.ext import Dispatcher, CallbackContext
import apscheduler.job # noqa: F401 import apscheduler.job # noqa: F401
@ -45,35 +42,15 @@ class JobQueue:
""" """
__slots__ = ('_dispatcher', 'logger', 'scheduler') __slots__ = ('_dispatcher', 'scheduler')
def __init__(self) -> None: def __init__(self) -> None:
self._dispatcher: 'Dispatcher' = None # type: ignore[assignment] self._dispatcher: 'Dispatcher' = None # type: ignore[assignment]
self.logger = logging.getLogger(self.__class__.__name__)
self.scheduler = BackgroundScheduler(timezone=pytz.utc) self.scheduler = BackgroundScheduler(timezone=pytz.utc)
self.scheduler.add_listener(
self._update_persistence, mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR
)
# Dispatch errors and don't log them in the APS logger
def aps_log_filter(record): # type: ignore
return 'raised an exception' not in record.msg
logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter)
self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR)
def _build_args(self, job: 'Job') -> List[CallbackContext]:
return [self._dispatcher.context_types.context.from_job(job, self._dispatcher)]
def _tz_now(self) -> datetime.datetime: def _tz_now(self) -> datetime.datetime:
return datetime.datetime.now(self.scheduler.timezone) return datetime.datetime.now(self.scheduler.timezone)
def _update_persistence(self, _: JobEvent) -> None:
self._dispatcher.update_persistence()
def _dispatch_error(self, event: JobEvent) -> None:
self._dispatcher.dispatch_error(None, event.exception)
@overload @overload
def _parse_time_input(self, time: None, shift_day: bool = False) -> None: def _parse_time_input(self, time: None, shift_day: bool = False) -> None:
... ...
@ -170,11 +147,11 @@ class JobQueue:
date_time = self._parse_time_input(when, shift_day=True) date_time = self._parse_time_input(when, shift_day=True)
j = self.scheduler.add_job( j = self.scheduler.add_job(
callback, job,
name=name, name=name,
trigger='date', trigger='date',
run_date=date_time, run_date=date_time,
args=self._build_args(job), args=(self._dispatcher,),
timezone=date_time.tzinfo or self.scheduler.timezone, timezone=date_time.tzinfo or self.scheduler.timezone,
**job_kwargs, **job_kwargs,
) )
@ -262,9 +239,9 @@ class JobQueue:
interval = interval.total_seconds() interval = interval.total_seconds()
j = self.scheduler.add_job( j = self.scheduler.add_job(
callback, job,
trigger='interval', trigger='interval',
args=self._build_args(job), args=(self._dispatcher,),
start_date=dt_first, start_date=dt_first,
end_date=dt_last, end_date=dt_last,
seconds=interval, seconds=interval,
@ -318,9 +295,9 @@ class JobQueue:
job = Job(callback, context, name) job = Job(callback, context, name)
j = self.scheduler.add_job( j = self.scheduler.add_job(
callback, job,
trigger='cron', trigger='cron',
args=self._build_args(job), args=(self._dispatcher,),
name=name, name=name,
day='last' if day == -1 else day, day='last' if day == -1 else day,
hour=when.hour, hour=when.hour,
@ -375,9 +352,9 @@ class JobQueue:
job = Job(callback, context, name) job = Job(callback, context, name)
j = self.scheduler.add_job( j = self.scheduler.add_job(
callback, job,
name=name, name=name,
args=self._build_args(job), args=(self._dispatcher,),
trigger='cron', trigger='cron',
day_of_week=','.join([str(d) for d in days]), day_of_week=','.join([str(d) for d in days]),
hour=time.hour, hour=time.hour,
@ -417,7 +394,7 @@ class JobQueue:
name = name or callback.__name__ name = name or callback.__name__
job = Job(callback, context, name) job = Job(callback, context, name)
j = self.scheduler.add_job(callback, args=self._build_args(job), name=name, **job_kwargs) j = self.scheduler.add_job(job, args=(self._dispatcher,), name=name, **job_kwargs)
job.job = j job.job = j
return job return job
@ -507,11 +484,39 @@ class Job:
self.job = cast(APSJob, job) # skipcq: PTC-W0052 self.job = cast(APSJob, job) # skipcq: PTC-W0052
def run(self, dispatcher: 'Dispatcher') -> None: def run(self, dispatcher: 'Dispatcher') -> None:
"""Executes the callback function independently of the jobs schedule.""" """Executes the callback function independently of the jobs schedule. Also calls
:meth:`telegram.ext.Dispatcher.update_persistence`.
.. versionchaged:: 14.0
Calls :meth:`telegram.ext.Dispatcher.update_persistence`.
Args:
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher this job is associated
with.
"""
try: try:
self.callback(dispatcher.context_types.context.from_job(self, dispatcher)) self.callback(dispatcher.context_types.context.from_job(self, dispatcher))
except Exception as exc: except Exception as exc:
dispatcher.dispatch_error(None, exc) dispatcher.dispatch_error(None, exc, job=self)
finally:
dispatcher.update_persistence(None)
def __call__(self, dispatcher: 'Dispatcher') -> None:
"""Shortcut for::
job.run(dispatcher)
Warning:
The fact that jobs are callable should be considered an implementation detail and not
as part of PTBs public API.
.. versionadded:: 14.0
Args:
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher this job is associated
with.
"""
self.run(dispatcher=dispatcher)
def schedule_removal(self) -> None: def schedule_removal(self) -> None:
""" """
@ -550,12 +555,7 @@ class Job:
@classmethod @classmethod
def _from_aps_job(cls, job: APSJob) -> 'Job': def _from_aps_job(cls, job: APSJob) -> 'Job':
# context based callbacks return job.func
if len(job.args) == 1:
context = job.args[0].job.context
else:
context = job.args[1].context
return cls(job.func, context=context, name=job.name, job=job)
def __getattr__(self, item: str) -> object: def __getattr__(self, item: str) -> object:
return getattr(self.job, item) return getattr(self.job, item)

View file

@ -95,7 +95,7 @@ class TestJobQueue:
self.result += 1 self.result += 1
def error_handler_context(self, update, context): def error_handler_context(self, update, context):
self.received_error = str(context.error) self.received_error = (str(context.error), context.job)
def error_handler_raise_error(self, *args): def error_handler_raise_error(self, *args):
raise Exception('Failing bigly') raise Exception('Failing bigly')
@ -425,10 +425,12 @@ class TestJobQueue:
job = job_queue.run_once(self.job_with_exception, 0.05) job = job_queue.run_once(self.job_with_exception, 0.05)
sleep(0.1) sleep(0.1)
assert self.received_error == 'Test Error' assert self.received_error[0] == 'Test Error'
assert self.received_error[1] is job
self.received_error = None self.received_error = None
job.run(dp) job.run(dp)
assert self.received_error == 'Test Error' assert self.received_error[0] == 'Test Error'
assert self.received_error[1] is job
# Remove handler # Remove handler
dp.remove_error_handler(self.error_handler_context) dp.remove_error_handler(self.error_handler_context)