mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-12-22 14:35:00 +01:00
Pass Failing Jobs to Error Handlers (#2692)
This commit is contained in:
parent
51488bb4be
commit
90b82eed99
5 changed files with 72 additions and 52 deletions
|
@ -6,3 +6,4 @@ telegram.ext.Job
|
||||||
.. autoclass:: telegram.ext.Job
|
.. autoclass:: telegram.ext.Job
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
:special-members: __call__
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue