From d2e768d0d0709a37db0952d721cad20a1dfd9c4a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 20 Dec 2020 20:46:29 +0100 Subject: [PATCH 01/31] Document CC constructors --- telegram/ext/callbackcontext.py | 88 ++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index 20ea6fb8812..008e20f77f4 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -50,20 +50,10 @@ class CallbackContext: almost certainly execute the callbacks for an update out of order, and the attributes that you think you added will not be present. - Attributes: - bot_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each - update it will be the same ``dict``. - chat_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each - update from the same chat id it will be the same ``dict``. - - Warning: - When a group chat migrates to a supergroup, its chat id will change and the - ``chat_data`` needs to be transferred. For details see our `wiki page - `_. + Args: + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context. - user_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each - update from the same user it will be the same ``dict``. + Attributes: matches (List[:obj:`re match object`]): Optional. If the associated update originated from a regex-supported handler or had a :class:`Filters.regex`, this will contain a list of match objects for every pattern where ``re.search(pattern, string)`` returned a match. @@ -114,6 +104,10 @@ def dispatcher(self) -> 'Dispatcher': @property def bot_data(self) -> Dict: + """ + bot_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each + update it will be the same :obj:`dict`. + """ return self._bot_data @bot_data.setter @@ -124,6 +118,17 @@ def bot_data(self, value: Any) -> NoReturn: @property def chat_data(self) -> Optional[Dict]: + """ + + chat_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each + update from the same chat id it will be the same :obj:`dict`. + + Warning: + When a group chat migrates to a supergroup, its chat id will change and the + ``chat_data`` needs to be transferred. For details see our `wiki page + `_. + """ return self._chat_data @chat_data.setter @@ -134,6 +139,10 @@ def chat_data(self, value: Any) -> NoReturn: @property def user_data(self) -> Optional[Dict]: + """ + user_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each + update from the same user it will be the same :obj:`dict`. + """ return self._user_data @user_data.setter @@ -151,6 +160,29 @@ def from_error( async_args: Union[List, Tuple] = None, async_kwargs: Dict[str, Any] = None, ) -> 'CallbackContext': + """ + Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error + handlers. + + .. seealso:: :meth:`telegram.ext.Dispatcher.add_error_handler` + + Args: + update (:obj:`any` | :class:`telegram.Update`): The update associated with the error. + May be :obj:`None`, e.g. for errors in job callbacks. + error (:obj:`Exception`): The error. + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this + context. + 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 + :meth:`telegram.ext.Dispatcher.run_async`. + 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 + asynchronously using :meth:`telegram.ext.Dispatcher.run_async`. + + Returns: + :class:`telegram.ext.CallbackContext` + + """ self = cls.from_update(update, dispatcher) self.error = error self.async_args = async_args @@ -159,6 +191,21 @@ def from_error( @classmethod def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackContext': + """ + Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the + handlers. + + .. seealso:: :meth:`telegram.ext.Dispatcher.add_handler` + + Args: + update (:obj:`any` | :class:`telegram.Update`): The update. + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this + context. + + Returns: + :class:`telegram.ext.CallbackContext` + + """ self = cls(dispatcher) if update is not None and isinstance(update, Update): @@ -173,6 +220,21 @@ def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackConte @classmethod def from_job(cls, job: 'Job', dispatcher: 'Dispatcher') -> 'CallbackContext': + """ + Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a + job callback. + + .. seealso:: :meth:`telegram.ext.JobQueue` + + Args: + job (:class:`telegram.ext.Job`): The job. + dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this + context. + + Returns: + :class:`telegram.ext.CallbackContext` + + """ self = cls(dispatcher) self.job = job return self From 4f8014f1b11941658e2d5b6a97b5ffb61cb11647 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Tue, 22 Dec 2020 13:34:43 +0100 Subject: [PATCH 02/31] Add custom_context parameter and try to sort out annotations --- examples/echobot.py | 2 +- telegram/ext/callbackqueryhandler.py | 9 ++- telegram/ext/choseninlineresulthandler.py | 3 +- telegram/ext/commandhandler.py | 14 ++-- telegram/ext/conversationhandler.py | 10 +-- telegram/ext/dispatcher.py | 95 ++++++++++++++++++++--- telegram/ext/handler.py | 11 +-- telegram/ext/inlinequeryhandler.py | 9 ++- telegram/ext/jobqueue.py | 4 +- telegram/ext/messagehandler.py | 9 ++- telegram/ext/pollanswerhandler.py | 3 +- telegram/ext/pollhandler.py | 3 +- telegram/ext/precheckoutqueryhandler.py | 3 +- telegram/ext/regexhandler.py | 5 +- telegram/ext/shippingqueryhandler.py | 3 +- telegram/ext/stringcommandhandler.py | 9 ++- telegram/ext/stringregexhandler.py | 9 ++- telegram/ext/typehandler.py | 10 +-- telegram/ext/updater.py | 30 ++++++- telegram/utils/types.py | 4 + 20 files changed, 176 insertions(+), 69 deletions(-) diff --git a/examples/echobot.py b/examples/echobot.py index c8a264584bd..bb9ceb855bd 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -42,7 +42,7 @@ def help_command(update: Update, context: CallbackContext) -> None: update.message.reply_text('Help!') -def echo(update: Update, context: CallbackContext) -> None: +def echo(update: str, context: CallbackContext) -> None: """Echo the user message.""" update.message.reply_text(update.message.text) diff --git a/telegram/ext/callbackqueryhandler.py b/telegram/ext/callbackqueryhandler.py index c229de76a2a..9c13f17ed06 100644 --- a/telegram/ext/callbackqueryhandler.py +++ b/telegram/ext/callbackqueryhandler.py @@ -36,14 +36,15 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler +from ..utils.types import CCT if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') -class CallbackQueryHandler(Handler[Update]): +class CallbackQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram callback queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. @@ -122,7 +123,7 @@ class CallbackQueryHandler(Handler[Update]): def __init__( self, - callback: Callable[[Update, 'CallbackContext'], RT], + callback: Callable[[Update, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pattern: Union[str, Pattern] = None, @@ -185,7 +186,7 @@ def collect_optional_args( def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: Update, dispatcher: 'Dispatcher', check_result: Union[bool, Match], diff --git a/telegram/ext/choseninlineresulthandler.py b/telegram/ext/choseninlineresulthandler.py index f65cb2e97b4..ff50c34974a 100644 --- a/telegram/ext/choseninlineresulthandler.py +++ b/telegram/ext/choseninlineresulthandler.py @@ -23,11 +23,12 @@ from telegram import Update from .handler import Handler +from ..utils.types import CCT RT = TypeVar('RT') -class ChosenInlineResultHandler(Handler[Update]): +class ChosenInlineResultHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a chosen inline result. Attributes: diff --git a/telegram/ext/commandhandler.py b/telegram/ext/commandhandler.py index 41a3d490702..bf1f4a07c8d 100644 --- a/telegram/ext/commandhandler.py +++ b/telegram/ext/commandhandler.py @@ -24,18 +24,18 @@ from telegram import MessageEntity, Update from telegram.ext import BaseFilter, Filters from telegram.utils.deprecate import TelegramDeprecationWarning -from telegram.utils.types import SLT +from telegram.utils.types import SLT, CCT from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') -class CommandHandler(Handler[Update]): +class CommandHandler(Handler[Update, CCT]): """Handler class to handle Telegram commands. Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the @@ -134,7 +134,7 @@ class CommandHandler(Handler[Update]): def __init__( self, command: SLT[str], - callback: Callable[[Update, 'CallbackContext'], RT], + callback: Callable[[Update, CCT], RT], filters: BaseFilter = None, allow_edited: bool = None, pass_args: bool = False, @@ -228,7 +228,7 @@ def collect_optional_args( def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: Update, dispatcher: 'Dispatcher', check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]], @@ -344,7 +344,7 @@ def __init__( self, prefix: SLT[str], command: SLT[str], - callback: Callable[[Update, 'CallbackContext'], RT], + callback: Callable[[Update, CCT], RT], filters: BaseFilter = None, pass_args: bool = False, pass_update_queue: bool = False, @@ -441,7 +441,7 @@ def check_update( def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: Update, dispatcher: 'Dispatcher', check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]], diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py index 8de9e58ec60..9e246de5479 100644 --- a/telegram/ext/conversationhandler.py +++ b/telegram/ext/conversationhandler.py @@ -35,7 +35,7 @@ InlineQueryHandler, ) from telegram.utils.promise import Promise -from telegram.utils.types import ConversationDict +from telegram.utils.types import ConversationDict, CCT if TYPE_CHECKING: from telegram.ext import Dispatcher, Job @@ -56,7 +56,7 @@ def __init__( self.callback_context = callback_context -class ConversationHandler(Handler[Update]): +class ConversationHandler(Handler[Update, CCT]): """ A handler to hold a conversation with a single user by managing four collections of other handlers. @@ -181,9 +181,9 @@ class ConversationHandler(Handler[Update]): # pylint: disable=W0231 def __init__( self, - entry_points: List[Handler], - states: Dict[object, List[Handler]], - fallbacks: List[Handler], + entry_points: List[Handler[Update, CCT]], + states: Dict[object, List[Handler[Update, CCT]]], + fallbacks: List[Handler[Update, CCT]], allow_reentry: bool = False, per_chat: bool = True, per_user: bool = True, diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index acee97c657e..76a7193bfb2 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -26,7 +26,21 @@ from queue import Empty, Queue from threading import BoundedSemaphore, Event, Lock, Thread, current_thread from time import sleep -from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Optional, Set, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + DefaultDict, + Dict, + List, + Optional, + Set, + Union, + Generic, + Type, + TypeVar, + overload, +) from uuid import uuid4 from telegram import TelegramError, Update @@ -36,6 +50,8 @@ from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from telegram.utils.types import CCT + if TYPE_CHECKING: from telegram import Bot @@ -43,6 +59,8 @@ DEFAULT_GROUP: int = 0 +UT = TypeVar('UT') + def run_async( func: Callable[[Update, CallbackContext], Any] @@ -103,7 +121,7 @@ def __init__(self, state: object = None) -> None: self.state = state -class Dispatcher: +class Dispatcher(Generic[CCT]): """This class dispatches all kinds of updates to its registered handlers. Attributes: @@ -118,6 +136,8 @@ class Dispatcher: bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot. persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to store data that should be persistent over restarts. + context_class (:obj:`class`): The class to use for the ``context`` argument of the + callbacks. Args: bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. @@ -131,6 +151,11 @@ class Dispatcher: use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. **New users**: set this to :obj:`True`. + custom_context (:obj:`class`, optional): Pass a subclass of + :class:`telegram.ext.CallbackContext` to be used instead of + :class:`telegram.ext.CallbackContext`, i.e. the ``context`` argument in all the + callbacks will be of this type instead. Defaults to + :class:`telegram.ext.CallbackContext`. """ @@ -139,6 +164,33 @@ class Dispatcher: __singleton = None logger = logging.getLogger(__name__) + @overload + def __init__( + self: 'Dispatcher'['CallbackContext'], # pylint: disable=E1126 + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True, + ): + ... + + @overload + def __init__( + self: 'Dispatcher'[CCT], # pylint: disable=E1126 + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True, + custom_context: Type[CCT] = None, + ): + ... + def __init__( self, bot: 'Bot', @@ -148,6 +200,7 @@ def __init__( job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, + custom_context: Type[CCT] = None, ): self.bot = bot self.update_queue = update_queue @@ -161,6 +214,16 @@ def __init__( TelegramDeprecationWarning, stacklevel=3, ) + if not use_context and custom_context: + raise ValueError( + 'Custom CallbackContext classes can only be used, when use_context=True.' + ) + if custom_context: + if not issubclass(custom_context, CallbackContext): + raise ValueError('custom_context must be a subclass of CallbackContext') + self.context_class = custom_context + else: + self.context_class = CallbackContext # type: ignore[assignment] self.user_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict) self.chat_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict) @@ -427,7 +490,7 @@ def process_update(self, update: Union[str, Update, TelegramError]) -> None: check = handler.check_update(update) if check is not None and check is not False: if not context and self.use_context: - context = CallbackContext.from_update(update, self) + context = self.context_class.from_update(update, self) handler.handle_update(update, self, check, context) # If handler runs async updating immediately doesn't make sense @@ -452,7 +515,7 @@ def process_update(self, update: Union[str, Update, TelegramError]) -> None: except Exception: self.logger.exception('An uncaught error was raised while handling the error.') - def add_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None: + def add_handler(self, handler: Handler[UT, CCT], group: int = DEFAULT_GROUP) -> None: """Register a handler. TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of @@ -484,14 +547,22 @@ def add_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None: raise TypeError(f'handler is not an instance of {Handler.__name__}') if not isinstance(group, int): raise TypeError('group is not int') - if isinstance(handler, ConversationHandler) and handler.persistent and handler.name: + # For some reason MyPy infers the type of handler is here, + # so for now we just ignore all the errors + if ( + isinstance(handler, ConversationHandler) + and handler.persistent # type: ignore[attr-defined] + and handler.name # type: ignore[attr-defined] + ): if not self.persistence: raise ValueError( - f"ConversationHandler {handler.name} can not be persistent if dispatcher has " - f"no persistence" + f"ConversationHandler {handler.name} " # type: ignore[attr-defined] + f"can not be persistent if dispatcher has no persistence" ) - handler.persistence = self.persistence - handler.conversations = self.persistence.get_conversations(handler.name) + handler.persistence = self.persistence # type: ignore[attr-defined] + handler.conversations = ( # type: ignore[attr-defined] + self.persistence.get_conversations(handler.name) # type: ignore[attr-defined] + ) if group not in self.handlers: self.handlers[group] = list() @@ -585,7 +656,7 @@ def __update_persistence(self, update: Any = None) -> None: def add_error_handler( self, - callback: Callable[[Any, CallbackContext], None], + callback: Callable[[Any, CCT], None], run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621 ) -> None: """Registers an error handler in the Dispatcher. This handler will receive every error @@ -621,7 +692,7 @@ def add_error_handler( self.error_handlers[callback] = run_async - def remove_error_handler(self, callback: Callable[[Any, CallbackContext], None]) -> None: + def remove_error_handler(self, callback: Callable[[Any, CCT], None]) -> None: """Removes an error handler. Args: @@ -648,7 +719,7 @@ def dispatch_error( if self.error_handlers: for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621 if self.use_context: - context = CallbackContext.from_error( + context = self.context_class.from_error( update, error, self, async_args=async_args, async_kwargs=async_kwargs ) if run_async: diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index 123ba543e0b..b0342121214 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -24,15 +24,16 @@ from telegram import Update from telegram.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from telegram.utils.types import CCT if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') UT = TypeVar('UT') -class Handler(Generic[UT], ABC): +class Handler(Generic[UT, CCT], ABC): """The base class for all update handlers. Create custom handlers by inheriting from it. Attributes: @@ -92,7 +93,7 @@ class Handler(Generic[UT], ABC): def __init__( self, - callback: Callable[[UT, 'CallbackContext'], RT], + callback: Callable[[UT, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, @@ -131,7 +132,7 @@ def handle_update( update: UT, dispatcher: 'Dispatcher', check_result: object, - context: 'CallbackContext' = None, + context: CCT = None, ) -> Union[RT, Promise]: """ This method is called if it was determined that an update should indeed @@ -168,7 +169,7 @@ def handle_update( def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: UT, dispatcher: 'Dispatcher', check_result: Any, diff --git a/telegram/ext/inlinequeryhandler.py b/telegram/ext/inlinequeryhandler.py index 4e30ca9910f..4420af07ff2 100644 --- a/telegram/ext/inlinequeryhandler.py +++ b/telegram/ext/inlinequeryhandler.py @@ -35,14 +35,15 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler +from ..utils.types import CCT if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') -class InlineQueryHandler(Handler[Update]): +class InlineQueryHandler(Handler[Update, CCT]): """ Handler class to handle Telegram inline queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. @@ -121,7 +122,7 @@ class InlineQueryHandler(Handler[Update]): def __init__( self, - callback: Callable[[Update, 'CallbackContext'], RT], + callback: Callable[[Update, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pattern: Union[str, Pattern] = None, @@ -186,7 +187,7 @@ def collect_optional_args( def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: Update, dispatcher: 'Dispatcher', check_result: Optional[Union[bool, Match]], diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index c176eaf25f4..8c391570884 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -70,7 +70,7 @@ def aps_log_filter(record): # type: ignore def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]: if self._dispatcher.use_context: - return [CallbackContext.from_job(job, self._dispatcher)] + return [self._dispatcher.context_class.from_job(job, self._dispatcher)] return [self._dispatcher.bot, job] def _tz_now(self) -> datetime.datetime: @@ -568,7 +568,7 @@ def run(self, dispatcher: 'Dispatcher') -> None: """Executes the callback function independently of the jobs schedule.""" try: if dispatcher.use_context: - self.callback(CallbackContext.from_job(self, dispatcher)) + self.callback(dispatcher.context_class.from_job(self, dispatcher)) else: self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg] except Exception as exc: diff --git a/telegram/ext/messagehandler.py b/telegram/ext/messagehandler.py index 10b05d0bf57..71450821aef 100644 --- a/telegram/ext/messagehandler.py +++ b/telegram/ext/messagehandler.py @@ -27,14 +27,15 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler +from ..utils.types import CCT if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') -class MessageHandler(Handler[Update]): +class MessageHandler(Handler[Update, CCT]): """Handler class to handle telegram messages. They might contain text, media or status updates. Attributes: @@ -123,7 +124,7 @@ class MessageHandler(Handler[Update]): def __init__( self, filters: BaseFilter, - callback: Callable[[Update, 'CallbackContext'], RT], + callback: Callable[[Update, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, @@ -195,7 +196,7 @@ def check_update(self, update: Any) -> Optional[Union[bool, Dict[str, Any]]]: def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: Update, dispatcher: 'Dispatcher', check_result: Optional[Union[bool, Dict[str, Any]]], diff --git a/telegram/ext/pollanswerhandler.py b/telegram/ext/pollanswerhandler.py index 17fd00ff00e..72ca7c40506 100644 --- a/telegram/ext/pollanswerhandler.py +++ b/telegram/ext/pollanswerhandler.py @@ -22,9 +22,10 @@ from telegram import Update from .handler import Handler +from ..utils.types import CCT -class PollAnswerHandler(Handler[Update]): +class PollAnswerHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a poll answer. Attributes: diff --git a/telegram/ext/pollhandler.py b/telegram/ext/pollhandler.py index 02f71edaeee..69f8f393fe9 100644 --- a/telegram/ext/pollhandler.py +++ b/telegram/ext/pollhandler.py @@ -22,9 +22,10 @@ from telegram import Update from .handler import Handler +from ..utils.types import CCT -class PollHandler(Handler[Update]): +class PollHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a poll. Attributes: diff --git a/telegram/ext/precheckoutqueryhandler.py b/telegram/ext/precheckoutqueryhandler.py index 332279d18c0..bd6c172861d 100644 --- a/telegram/ext/precheckoutqueryhandler.py +++ b/telegram/ext/precheckoutqueryhandler.py @@ -22,9 +22,10 @@ from telegram import Update from .handler import Handler +from ..utils.types import CCT -class PreCheckoutQueryHandler(Handler[Update]): +class PreCheckoutQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram PreCheckout callback queries. Attributes: diff --git a/telegram/ext/regexhandler.py b/telegram/ext/regexhandler.py index ab6432843de..3ce4b165737 100644 --- a/telegram/ext/regexhandler.py +++ b/telegram/ext/regexhandler.py @@ -26,9 +26,10 @@ from telegram.ext import Filters, MessageHandler from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from telegram.utils.types import CCT if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') @@ -111,7 +112,7 @@ class RegexHandler(MessageHandler): def __init__( self, pattern: Union[str, Pattern], - callback: Callable[[Update, 'CallbackContext'], RT], + callback: Callable[[Update, CCT], RT], pass_groups: bool = False, pass_groupdict: bool = False, pass_update_queue: bool = False, diff --git a/telegram/ext/shippingqueryhandler.py b/telegram/ext/shippingqueryhandler.py index d5a5d372538..d82aafe28e3 100644 --- a/telegram/ext/shippingqueryhandler.py +++ b/telegram/ext/shippingqueryhandler.py @@ -21,9 +21,10 @@ from telegram import Update from .handler import Handler +from ..utils.types import CCT -class ShippingQueryHandler(Handler[Update]): +class ShippingQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram shipping callback queries. Attributes: diff --git a/telegram/ext/stringcommandhandler.py b/telegram/ext/stringcommandhandler.py index 068cbb7b2b4..80179d16c25 100644 --- a/telegram/ext/stringcommandhandler.py +++ b/telegram/ext/stringcommandhandler.py @@ -23,14 +23,15 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler +from ..utils.types import CCT if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') -class StringCommandHandler(Handler[str]): +class StringCommandHandler(Handler[str, CCT]): """Handler class to handle string commands. Commands are string updates that start with ``/``. Note: @@ -85,7 +86,7 @@ class StringCommandHandler(Handler[str]): def __init__( self, command: str, - callback: Callable[[str, 'CallbackContext'], RT], + callback: Callable[[str, CCT], RT], pass_args: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, @@ -129,7 +130,7 @@ def collect_optional_args( def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: str, dispatcher: 'Dispatcher', check_result: Optional[List[str]], diff --git a/telegram/ext/stringregexhandler.py b/telegram/ext/stringregexhandler.py index 6caac3d3861..d40ccd3f104 100644 --- a/telegram/ext/stringregexhandler.py +++ b/telegram/ext/stringregexhandler.py @@ -24,14 +24,15 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler +from ..utils.types import CCT if TYPE_CHECKING: - from telegram.ext import CallbackContext, Dispatcher + from telegram.ext import Dispatcher RT = TypeVar('RT') -class StringRegexHandler(Handler[str]): +class StringRegexHandler(Handler[str, CCT]): """Handler class to handle string updates based on a regex which checks the update content. Read the documentation of the ``re`` module for more information. The ``re.match`` function is @@ -94,7 +95,7 @@ class StringRegexHandler(Handler[str]): def __init__( self, pattern: Union[str, Pattern], - callback: Callable[[str, 'CallbackContext'], RT], + callback: Callable[[str, CCT], RT], pass_groups: bool = False, pass_groupdict: bool = False, pass_update_queue: bool = False, @@ -147,7 +148,7 @@ def collect_optional_args( def collect_additional_context( self, - context: 'CallbackContext', + context: CCT, update: str, dispatcher: 'Dispatcher', check_result: Optional[Match], diff --git a/telegram/ext/typehandler.py b/telegram/ext/typehandler.py index 7d40a0b5b47..1e3d1bc718b 100644 --- a/telegram/ext/typehandler.py +++ b/telegram/ext/typehandler.py @@ -18,19 +18,17 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the TypeHandler class.""" -from typing import TYPE_CHECKING, Any, Callable, Type, TypeVar, Union +from typing import Any, Callable, Type, TypeVar, Union from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler - -if TYPE_CHECKING: - from telegram.ext import CallbackContext +from ..utils.types import CCT RT = TypeVar('RT') UT = TypeVar('UT') -class TypeHandler(Handler[UT]): +class TypeHandler(Handler[UT, CCT]): """Handler class to handle updates of custom types. Attributes: @@ -78,7 +76,7 @@ class TypeHandler(Handler[UT]): def __init__( self, type: Type[UT], # pylint: disable=W0622 - callback: Callable[[UT, 'CallbackContext'], RT], + callback: Callable[[UT, CCT], RT], strict: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 4f258885005..08bcfac9d87 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -25,7 +25,19 @@ from signal import SIGABRT, SIGINT, SIGTERM, signal from threading import Event, Lock, Thread, current_thread from time import sleep -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union, no_type_check +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Union, + no_type_check, + Type, + Generic, +) from telegram import Bot, TelegramError from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized @@ -33,13 +45,14 @@ from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import get_signal_name from telegram.utils.request import Request +from telegram.utils.types import CCT from telegram.utils.webhookhandler import WebhookAppClass, WebhookServer if TYPE_CHECKING: from telegram.ext import BasePersistence, Defaults -class Updater: +class Updater(Generic[CCT]): """ This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to @@ -94,6 +107,11 @@ class Updater: used). defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to be used if not set explicitly in the bot methods. + custom_context (:obj:`class`, optional): Pass a subclass of + :class:`telegram.ext.CallbackContext` to be used instead of + :class:`telegram.ext.CallbackContext`, i.e. the ``context`` argument in all the + callbacks will be of this type instead. Defaults to + :class:`telegram.ext.CallbackContext`. Note: * You must supply either a :attr:`bot` or a :attr:`token` argument. @@ -120,8 +138,9 @@ def __init__( persistence: 'BasePersistence' = None, defaults: 'Defaults' = None, use_context: bool = True, - dispatcher: Dispatcher = None, + dispatcher: Dispatcher[CCT] = None, base_file_url: str = None, + custom_context: Type[CCT] = None, ): if defaults and bot: @@ -148,6 +167,8 @@ def __init__( raise ValueError('`dispatcher` and `workers` are mutually exclusive') if use_context != dispatcher.use_context: raise ValueError('`dispatcher` and `use_context` are mutually exclusive') + if custom_context != dispatcher.context_class: + raise ValueError('`dispatcher` and `custom_context` are mutually exclusive') self.logger = logging.getLogger(__name__) @@ -186,7 +207,7 @@ def __init__( self.job_queue = JobQueue() self.__exception_event = Event() self.persistence = persistence - self.dispatcher = Dispatcher( + self.dispatcher: Dispatcher[CCT] = Dispatcher( self.bot, self.update_queue, job_queue=self.job_queue, @@ -194,6 +215,7 @@ def __init__( exception_event=self.__exception_event, persistence=persistence, use_context=use_context, + custom_context=custom_context, ) self.job_queue.set_dispatcher(self.dispatcher) else: diff --git a/telegram/utils/types.py b/telegram/utils/types.py index 0a6ba598a1f..8de1262b98c 100644 --- a/telegram/utils/types.py +++ b/telegram/utils/types.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from telegram import InputFile + from telegram.ext import CallbackContext # noqa: F401 FileLike = Union[IO, 'InputFile'] """Either an open file handler or a :class:`telegram.InputFile`.""" @@ -39,3 +40,6 @@ RT = TypeVar("RT") SLT = Union[RT, List[RT], Tuple[RT, ...]] """Single instance or list/tuple of instances.""" + +CCT = TypeVar('CCT', bound='CallbackContext') +"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.""" From 680ef7ae0b7ccded8694491c5919d212fb26d951 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Tue, 22 Dec 2020 14:07:33 +0100 Subject: [PATCH 03/31] Add tests --- telegram/ext/dispatcher.py | 4 ++-- tests/test_dispatcher.py | 27 +++++++++++++++++++++++++++ tests/test_jobqueue.py | 17 ++++++++++++++++- tests/test_updater.py | 6 ++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 76a7193bfb2..e776f50d9cb 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -166,7 +166,7 @@ class Dispatcher(Generic[CCT]): @overload def __init__( - self: 'Dispatcher'['CallbackContext'], # pylint: disable=E1126 + self: 'Dispatcher[CallbackContext]', bot: 'Bot', update_queue: Queue, workers: int = 4, @@ -179,7 +179,7 @@ def __init__( @overload def __init__( - self: 'Dispatcher'[CCT], # pylint: disable=E1126 + self: 'Dispatcher[CCT]', bot: 'Bot', update_queue: Queue, workers: int = 4, diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 3ce3f34c605..6c5865d3af9 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -45,6 +45,10 @@ def dp2(bot): yield dp +class CustomContext(CallbackContext): + pass + + class TestDispatcher: message_update = Update( 1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') @@ -804,3 +808,26 @@ def callback(update, context): assert cdp.persistence.test_flag_bot_data assert not cdp.persistence.test_flag_user_data assert cdp.persistence.test_flag_chat_data + + def test_custom_context_error_handler(self, bot): + def error_handler(_, context): + self.received = type(context) + + dispatcher = Dispatcher(bot, Queue(), custom_context=CustomContext) + dispatcher.add_error_handler(error_handler) + dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) + + dispatcher.process_update(self.message_update) + sleep(0.1) + assert self.received == CustomContext + + def test_custom_context_handler_callback(self, bot): + def callback(_, context): + self.received = type(context) + + dispatcher = Dispatcher(bot, Queue(), custom_context=CustomContext) + dispatcher.add_handler(MessageHandler(Filters.all, callback)) + + dispatcher.process_update(self.message_update) + sleep(0.1) + assert self.received == CustomContext diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 183d1a02b83..622980152a7 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -28,7 +28,11 @@ import pytz from apscheduler.schedulers import SchedulerNotRunningError from flaky import flaky -from telegram.ext import JobQueue, Updater, Job, CallbackContext +from telegram.ext import JobQueue, Updater, Job, CallbackContext, Dispatcher + + +class CustomContext(CallbackContext): + pass @pytest.fixture(scope='function') @@ -510,3 +514,14 @@ def test_dispatch_error_that_raises_errors(self, job_queue, dp, caplog): assert len(caplog.records) == 1 rec = caplog.records[-1] assert 'No error handlers are registered' in rec.getMessage() + + def test_custom_context(self, bot, job_queue): + dispatcher = Dispatcher(bot, Queue(), custom_context=CustomContext) + job_queue.set_dispatcher(dispatcher) + + def callback(context): + self.result = type(context) + + job_queue.run_once(callback, 0.1) + sleep(0.15) + assert self.result == CustomContext diff --git a/tests/test_updater.py b/tests/test_updater.py index 745836acd3d..5c20db4f01f 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -580,6 +580,12 @@ def test_mutual_exclude_use_context_dispatcher(self): with pytest.raises(ValueError): Updater(dispatcher=dispatcher, use_context=use_context) + def test_mutual_exclude_custom_context_dispatcher(self): + dispatcher = Dispatcher(None, None) + custom_context = object + with pytest.raises(ValueError): + Updater(dispatcher=dispatcher, custom_context=custom_context) + def test_defaults_warning(self, bot): with pytest.warns(TelegramDeprecationWarning, match='no effect when a Bot is passed'): Updater(bot=bot, defaults=Defaults()) From e1eb5850d850a9e74e3e78b4f8b3bcae2640a936 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Tue, 22 Dec 2020 19:38:21 +0100 Subject: [PATCH 04/31] POC for customizing *_data --- .../source/telegram.ext.contextcustomizer.rst | 6 + docs/source/telegram.ext.rst | 1 + telegram/ext/__init__.py | 2 + telegram/ext/basepersistence.py | 44 +++--- telegram/ext/callbackcontext.py | 15 +- telegram/ext/contextcustomizer.py | 128 ++++++++++++++++++ telegram/ext/dispatcher.py | 101 +++++--------- telegram/ext/jobqueue.py | 4 +- telegram/ext/updater.py | 27 ++-- telegram/utils/types.py | 13 +- tests/test_dispatcher.py | 9 +- tests/test_jobqueue.py | 6 +- tests/test_updater.py | 3 +- 13 files changed, 242 insertions(+), 117 deletions(-) create mode 100644 docs/source/telegram.ext.contextcustomizer.rst create mode 100644 telegram/ext/contextcustomizer.py diff --git a/docs/source/telegram.ext.contextcustomizer.rst b/docs/source/telegram.ext.contextcustomizer.rst new file mode 100644 index 00000000000..505c2c0c1c7 --- /dev/null +++ b/docs/source/telegram.ext.contextcustomizer.rst @@ -0,0 +1,6 @@ +telegram.ext.ContextCustomizer +============================== + +.. autoclass:: telegram.ext.ContextCustomizer + :members: + :show-inheritance: diff --git a/docs/source/telegram.ext.rst b/docs/source/telegram.ext.rst index d5148bd6122..1afa0087f56 100644 --- a/docs/source/telegram.ext.rst +++ b/docs/source/telegram.ext.rst @@ -13,6 +13,7 @@ telegram.ext package telegram.ext.delayqueue telegram.ext.callbackcontext telegram.ext.defaults + telegram.ext.contextcustomizer Handlers -------- diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index b614e292c74..0b675d1b0d6 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -23,6 +23,7 @@ from .dictpersistence import DictPersistence from .handler import Handler from .callbackcontext import CallbackContext +from .contextcustomizer import ContextCustomizer from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async from .jobqueue import JobQueue, Job from .updater import Updater @@ -79,4 +80,5 @@ 'PollAnswerHandler', 'PollHandler', 'Defaults', + 'ContextCustomizer', ) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 378110ed630..f5b9798eaf3 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -20,14 +20,14 @@ import warnings from abc import ABC, abstractmethod from copy import copy -from typing import Any, DefaultDict, Dict, Optional, Tuple, cast, ClassVar +from typing import Any, DefaultDict, Dict, Optional, Tuple, cast, ClassVar, Generic, Mapping from telegram import Bot -from telegram.utils.types import ConversationDict +from telegram.utils.types import ConversationDict, CD, UD, BD, UDM, CDM -class BasePersistence(ABC): +class BasePersistence(ABC, Generic[UD, CD, BD, UDM, CDM]): """Interface class for adding persistence to your bot. Subclass this object for different implementations of a persistent bot. @@ -84,22 +84,22 @@ def __new__(cls, *args: Any, **kwargs: Any) -> 'BasePersistence': # pylint: dis update_chat_data = instance.update_chat_data update_bot_data = instance.update_bot_data - def get_user_data_insert_bot() -> DefaultDict[int, Dict[Any, Any]]: + def get_user_data_insert_bot() -> UDM[int, UD]: return instance.insert_bot(get_user_data()) - def get_chat_data_insert_bot() -> DefaultDict[int, Dict[Any, Any]]: + def get_chat_data_insert_bot() -> CDM[int, CD]: return instance.insert_bot(get_chat_data()) - def get_bot_data_insert_bot() -> Dict[Any, Any]: + def get_bot_data_insert_bot() -> BD: return instance.insert_bot(get_bot_data()) - def update_user_data_replace_bot(user_id: int, data: Dict) -> None: + def update_user_data_replace_bot(user_id: int, data: UD) -> None: return update_user_data(user_id, instance.replace_bot(data)) - def update_chat_data_replace_bot(chat_id: int, data: Dict) -> None: + def update_chat_data_replace_bot(chat_id: int, data: CD) -> None: return update_chat_data(chat_id, instance.replace_bot(data)) - def update_bot_data_replace_bot(data: Dict) -> None: + def update_bot_data_replace_bot(data: BD) -> None: return update_bot_data(instance.replace_bot(data)) instance.get_user_data = get_user_data_insert_bot @@ -288,27 +288,27 @@ def _insert_bot(self, obj: object, memo: Dict[int, Any]) -> object: # pylint: d return obj @abstractmethod - def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]: + def get_user_data(self) -> UDM[int, UD]: """ "Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``user_data`` if stored, or an empty - ``defaultdict(dict)``. + ``Mapping`` with integer keys. Returns: - :obj:`defaultdict`: The restored user data. + :obj:`Mapping[int, UD]`: The restored user data. """ @abstractmethod - def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]: + def get_chat_data(self) -> CDM[int, CD]: """ "Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``chat_data`` if stored, or an empty - ``defaultdict(dict)``. + ``Mapping`` with integer keys. Returns: - :obj:`defaultdict`: The restored chat data. + :obj:`Mapping[int, Any]`: The restored chat data. """ @abstractmethod - def get_bot_data(self) -> Dict[Any, Any]: + def get_bot_data(self) -> BD: """ "Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``bot_data`` if stored, or an empty :obj:`dict`. @@ -345,32 +345,32 @@ def update_conversation( """ @abstractmethod - def update_user_data(self, user_id: int, data: Dict) -> None: + def update_user_data(self, user_id: int, data: UD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. Args: user_id (:obj:`int`): The user the data might have been changed for. - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. + data (:obj:`UD`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. """ @abstractmethod - def update_chat_data(self, chat_id: int, data: Dict) -> None: + def update_chat_data(self, chat_id: int, data: CD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. Args: chat_id (:obj:`int`): The chat the data might have been changed for. - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. + data (:obj:`CD`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. """ @abstractmethod - def update_bot_data(self, data: Dict) -> None: + def update_bot_data(self, data: BD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. Args: - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data` . + data (:obj:`BD`): The :attr:`telegram.ext.dispatcher.bot_data` . """ def flush(self) -> None: diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index 008e20f77f4..3cd816b589c 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -19,16 +19,17 @@ # pylint: disable=R0201 """This module contains the CallbackContext class.""" from queue import Queue -from typing import TYPE_CHECKING, Any, Dict, List, Match, NoReturn, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Match, NoReturn, Optional, Tuple, Union, Generic from telegram import Update +from telegram.utils.types import UD, CD, BD if TYPE_CHECKING: from telegram import Bot from telegram.ext import Dispatcher, Job, JobQueue -class CallbackContext: +class CallbackContext(Generic[UD, CD, BD]): """ This is a context object passed to the callback called by :class:`telegram.ext.Handler` or by the :class:`telegram.ext.Dispatcher` in an error handler added by @@ -88,8 +89,8 @@ def __init__(self, dispatcher: 'Dispatcher'): ) self._dispatcher = dispatcher self._bot_data = dispatcher.bot_data - self._chat_data: Optional[Dict[Any, Any]] = None - self._user_data: Optional[Dict[Any, Any]] = None + self._chat_data: Optional[CD] = None + self._user_data: Optional[UD] = None self.args: Optional[List[str]] = None self.matches: Optional[List[Match]] = None self.error: Optional[Exception] = None @@ -103,7 +104,7 @@ def dispatcher(self) -> 'Dispatcher': return self._dispatcher @property - def bot_data(self) -> Dict: + def bot_data(self) -> BD: """ bot_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each update it will be the same :obj:`dict`. @@ -117,7 +118,7 @@ def bot_data(self, value: Any) -> NoReturn: ) @property - def chat_data(self) -> Optional[Dict]: + def chat_data(self) -> Optional[CD]: """ chat_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each @@ -138,7 +139,7 @@ def chat_data(self, value: Any) -> NoReturn: ) @property - def user_data(self) -> Optional[Dict]: + def user_data(self) -> Optional[UD]: """ user_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each update from the same user it will be the same :obj:`dict`. diff --git a/telegram/ext/contextcustomizer.py b/telegram/ext/contextcustomizer.py new file mode 100644 index 00000000000..0d8e5fb4281 --- /dev/null +++ b/telegram/ext/contextcustomizer.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2020 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains the auxiliary class ContextCustomizer.""" +from collections import defaultdict, Mapping +from typing import Type, Optional, Any, NoReturn, Generic + +from telegram.ext.callbackcontext import CallbackContext +from telegram.utils.types import CCT, UD, CD, BD, CDM, UDM + + +class ContextCustomizer(Generic[CCT, UD, CD, BD, UDM, CDM]): + """ + Convenience class to gather customizable types of the ``context`` interface. + + Args: + context (:obj:`type`, optional): Determines the type of the ``context`` argument of all + (error-)handler callbacks and job callbacks. Must be a subclass of + :class:`telegram.ext.CallbackContext`. Defaults to + :class:`telegram.ext.CallbackContext`. + bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments + chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + user_data (:obj:`type`, optional): Determines the type of ``context.user_data` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + chat_data_mapping (:obj:`type`, optional): In combination with :attr:`chat_data` determines + the type of :attr:`telegram.ext.Dispatcher.chat_data`. Must support instantiating via + + .. code:: python + + chat_data_mapping(chat_data) + + user_data_mapping (:obj:`type`, optional): In combination with :attr:`user_data` determines + the type of :attr:`telegram.ext.Dispatcher.user_data`. Must support instantiating via + + .. code:: python + + user_data_mapping(user_data) + + """ + + def __init__( + self, + context: Type[CCT] = CallbackContext, + bot_data: Type[BD] = dict, + chat_data: Type[CD] = dict, + user_data: Type[UD] = dict, + chat_data_mapping: Type[CDM] = defaultdict, + user_data_mapping: Type[UDM] = defaultdict, + ): + if not issubclass(context, CallbackContext): + raise ValueError('context must be a subclass of CallbackContext.') + if not issubclass(chat_data_mapping, Mapping): + raise ValueError('chat_data_mapping must be a subclass of collections.Mapping.') + if not issubclass(user_data_mapping, Mapping): + raise ValueError('user_data_mapping must be a subclass of collections.Mapping.') + + self._context = context + self._bot_data = bot_data + self._chat_data = chat_data + self._user_data = user_data + self._chat_data_mapping = chat_data_mapping + self._user_data_mapping = user_data_mapping + + @property + def context(self) -> Optional[Type]: + return self._context + + @context.setter + def context(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") + + @property + def bot_data(self) -> Optional[Type]: + return self._bot_data + + @bot_data.setter + def bot_data(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") + + @property + def chat_data(self) -> Optional[Type]: + return self._chat_data + + @chat_data.setter + def chat_data(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") + + @property + def user_data(self) -> Optional[Type]: + return self._user_data + + @user_data.setter + def user_data(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") + + @property + def user_data_mapping(self) -> Optional[Type]: + return self._user_data_mapping + + @user_data_mapping.setter + def user_data_mapping(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") + + @property + def chat_data_mapping(self) -> Optional[Type]: + return self._chat_data_mapping + + @chat_data_mapping.setter + def chat_data_mapping(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index e776f50d9cb..8300d2ca9cc 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -21,7 +21,6 @@ import logging import warnings import weakref -from collections import defaultdict from functools import wraps from queue import Empty, Queue from threading import BoundedSemaphore, Event, Lock, Thread, current_thread @@ -30,28 +29,24 @@ TYPE_CHECKING, Any, Callable, - DefaultDict, Dict, List, Optional, Set, Union, Generic, - Type, TypeVar, - overload, ) from uuid import uuid4 from telegram import TelegramError, Update -from telegram.ext import BasePersistence +from telegram.ext import BasePersistence, ContextCustomizer from telegram.ext.callbackcontext import CallbackContext from telegram.ext.handler import Handler from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE -from telegram.utils.types import CCT - +from telegram.utils.types import CCT, UD, CD, BD, UDM, CDM if TYPE_CHECKING: from telegram import Bot @@ -121,7 +116,7 @@ def __init__(self, state: object = None) -> None: self.state = state -class Dispatcher(Generic[CCT]): +class Dispatcher(Generic[CCT, UD, CD, BD, UDM, CDM]): """This class dispatches all kinds of updates to its registered handlers. Attributes: @@ -136,8 +131,8 @@ class Dispatcher(Generic[CCT]): bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot. persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to store data that should be persistent over restarts. - context_class (:obj:`class`): The class to use for the ``context`` argument of the - callbacks. + context_customizer (:class:`telegram.ext.ContextCustomizer`): Container for the types used + in the ``context`` interface. Args: bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. @@ -151,11 +146,10 @@ class Dispatcher(Generic[CCT]): use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. **New users**: set this to :obj:`True`. - custom_context (:obj:`class`, optional): Pass a subclass of - :class:`telegram.ext.CallbackContext` to be used instead of - :class:`telegram.ext.CallbackContext`, i.e. the ``context`` argument in all the - callbacks will be of this type instead. Defaults to - :class:`telegram.ext.CallbackContext`. + context_customizer (:class:`telegram.ext.ContextCustomizer`, optional): Pass an instance + of :class:`telegram.ext.ContextCustomizer` to customize the the types used in the + ``context`` interface. If not passed, the defaults documented in + :class:`telegram.ext.ContextCustomizer` will be used. """ @@ -164,33 +158,6 @@ class Dispatcher(Generic[CCT]): __singleton = None logger = logging.getLogger(__name__) - @overload - def __init__( - self: 'Dispatcher[CallbackContext]', - bot: 'Bot', - update_queue: Queue, - workers: int = 4, - exception_event: Event = None, - job_queue: 'JobQueue' = None, - persistence: BasePersistence = None, - use_context: bool = True, - ): - ... - - @overload - def __init__( - self: 'Dispatcher[CCT]', - bot: 'Bot', - update_queue: Queue, - workers: int = 4, - exception_event: Event = None, - job_queue: 'JobQueue' = None, - persistence: BasePersistence = None, - use_context: bool = True, - custom_context: Type[CCT] = None, - ): - ... - def __init__( self, bot: 'Bot', @@ -198,15 +165,16 @@ def __init__( workers: int = 4, exception_event: Event = None, job_queue: 'JobQueue' = None, - persistence: BasePersistence = None, + persistence: BasePersistence[UD, CD, BD, UDM, CDM] = None, use_context: bool = True, - custom_context: Type[CCT] = None, + context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = ContextCustomizer(), ): self.bot = bot self.update_queue = update_queue self.job_queue = job_queue self.workers = workers self.use_context = use_context + self.context_customizer = context_customizer if not use_context: warnings.warn( @@ -214,20 +182,14 @@ def __init__( TelegramDeprecationWarning, stacklevel=3, ) - if not use_context and custom_context: - raise ValueError( - 'Custom CallbackContext classes can only be used, when use_context=True.' - ) - if custom_context: - if not issubclass(custom_context, CallbackContext): - raise ValueError('custom_context must be a subclass of CallbackContext') - self.context_class = custom_context - else: - self.context_class = CallbackContext # type: ignore[assignment] - self.user_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict) - self.chat_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict) - self.bot_data = {} + self.user_data = self.context_customizer.user_data_mapping( + self.context_customizer.user_data + ) + self.chat_data = self.context_customizer.chat_data_mapping( + self.context_customizer.chat_data + ) + self.bot_data = self.context_customizer.bot_data() self.persistence: Optional[BasePersistence] = None self._update_persistence_lock = Lock() if persistence: @@ -237,16 +199,25 @@ def __init__( self.persistence.set_bot(self.bot) if self.persistence.store_user_data: self.user_data = self.persistence.get_user_data() - if not isinstance(self.user_data, defaultdict): - raise ValueError("user_data must be of type defaultdict") + if not isinstance(self.user_data, self.context_customizer.user_data_mapping): + raise ValueError( + f"user_data must be of type " + f"{self.context_customizer.user_data_mapping.__name__}" + ) if self.persistence.store_chat_data: self.chat_data = self.persistence.get_chat_data() - if not isinstance(self.chat_data, defaultdict): - raise ValueError("chat_data must be of type defaultdict") + if not isinstance(self.chat_data, self.context_customizer.chat_data_mapping): + raise ValueError( + f"chat_data must be of type " + f"{self.context_customizer.chat_data_mapping.__name__}" + ) if self.persistence.store_bot_data: self.bot_data = self.persistence.get_bot_data() - if not isinstance(self.bot_data, dict): - raise ValueError("bot_data must be of type dict") + if not isinstance(self.bot_data, self.context_customizer.bot_data): + raise ValueError( + f"bot_data must be of type {self.context_customizer.bot_data.__name__}" + ) + else: self.persistence = None @@ -490,7 +461,7 @@ def process_update(self, update: Union[str, Update, TelegramError]) -> None: check = handler.check_update(update) if check is not None and check is not False: if not context and self.use_context: - context = self.context_class.from_update(update, self) + context = self.context_customizer.context.from_update(update, self) handler.handle_update(update, self, check, context) # If handler runs async updating immediately doesn't make sense @@ -719,7 +690,7 @@ def dispatch_error( if self.error_handlers: for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621 if self.use_context: - context = self.context_class.from_error( + context = self.context_customizer.context.from_error( update, error, self, async_args=async_args, async_kwargs=async_kwargs ) if run_async: diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 8c391570884..a06659f969a 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -70,7 +70,7 @@ def aps_log_filter(record): # type: ignore def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]: if self._dispatcher.use_context: - return [self._dispatcher.context_class.from_job(job, self._dispatcher)] + return [self._dispatcher.context_customizer.context.from_job(job, self._dispatcher)] return [self._dispatcher.bot, job] def _tz_now(self) -> datetime.datetime: @@ -568,7 +568,7 @@ def run(self, dispatcher: 'Dispatcher') -> None: """Executes the callback function independently of the jobs schedule.""" try: if dispatcher.use_context: - self.callback(dispatcher.context_class.from_job(self, dispatcher)) + self.callback(dispatcher.context_customizer.context.from_job(self, dispatcher)) else: self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg] except Exception as exc: diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 08bcfac9d87..00085cb1c66 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -41,18 +41,18 @@ from telegram import Bot, TelegramError from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized -from telegram.ext import Dispatcher, JobQueue +from telegram.ext import Dispatcher, JobQueue, ContextCustomizer from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import get_signal_name from telegram.utils.request import Request -from telegram.utils.types import CCT +from telegram.utils.types import CCT, UD, BD, UDM, CDM, CD from telegram.utils.webhookhandler import WebhookAppClass, WebhookServer if TYPE_CHECKING: from telegram.ext import BasePersistence, Defaults -class Updater(Generic[CCT]): +class Updater(Generic[CCT, UD, CD, BD, UDM, CDM]): """ This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to @@ -107,11 +107,10 @@ class Updater(Generic[CCT]): used). defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to be used if not set explicitly in the bot methods. - custom_context (:obj:`class`, optional): Pass a subclass of - :class:`telegram.ext.CallbackContext` to be used instead of - :class:`telegram.ext.CallbackContext`, i.e. the ``context`` argument in all the - callbacks will be of this type instead. Defaults to - :class:`telegram.ext.CallbackContext`. + context_customizer (:class:`telegram.ext.ContextCustomizer`, optional): Pass an instance + of :class:`telegram.ext.ContextCustomizer` to customize the the types used in the + ``context`` interface. If not passed, the defaults documented in + :class:`telegram.ext.ContextCustomizer` will be used. Note: * You must supply either a :attr:`bot` or a :attr:`token` argument. @@ -135,12 +134,12 @@ def __init__( private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, - persistence: 'BasePersistence' = None, + persistence: 'BasePersistence[UD, CD, BD, UDM, CDM]' = None, defaults: 'Defaults' = None, use_context: bool = True, - dispatcher: Dispatcher[CCT] = None, + dispatcher: Dispatcher[CCT, UD, CD, BD, UDM, CDM] = None, base_file_url: str = None, - custom_context: Type[CCT] = None, + context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, ): if defaults and bot: @@ -167,8 +166,8 @@ def __init__( raise ValueError('`dispatcher` and `workers` are mutually exclusive') if use_context != dispatcher.use_context: raise ValueError('`dispatcher` and `use_context` are mutually exclusive') - if custom_context != dispatcher.context_class: - raise ValueError('`dispatcher` and `custom_context` are mutually exclusive') + if context_customizer is not None: + raise ValueError('`dispatcher` and `context_customizer` are mutually exclusive') self.logger = logging.getLogger(__name__) @@ -215,7 +214,7 @@ def __init__( exception_event=self.__exception_event, persistence=persistence, use_context=use_context, - custom_context=custom_context, + context_customizer=context_customizer or ContextCustomizer(), ) self.job_queue.set_dispatcher(self.dispatcher) else: diff --git a/telegram/utils/types.py b/telegram/utils/types.py index 8de1262b98c..80e4e2f8680 100644 --- a/telegram/utils/types.py +++ b/telegram/utils/types.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains custom typing aliases.""" from pathlib import Path -from typing import IO, TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar, Union +from typing import IO, TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar, Union, Mapping if TYPE_CHECKING: from telegram import InputFile @@ -43,3 +43,14 @@ CCT = TypeVar('CCT', bound='CallbackContext') """An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.""" + +UD = TypeVar('UD') +"""Type of the user data for a single user.""" +CD = TypeVar('CD') +"""Type of the chat data for a single user.""" +BD = TypeVar('BD') +"""Type of the bot data.""" +UDM = TypeVar('UDM', bound=Mapping) +"""Type of the user data mapping.""" +CDM = TypeVar('CDM', bound=Mapping) +"""Type of the chat data mapping.""" diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 6c5865d3af9..f327a4b721d 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -32,6 +32,7 @@ CallbackContext, JobQueue, BasePersistence, + ContextCustomizer, ) from telegram.ext.dispatcher import run_async, Dispatcher, DispatcherHandlerStop from telegram.utils.deprecate import TelegramDeprecationWarning @@ -813,7 +814,9 @@ def test_custom_context_error_handler(self, bot): def error_handler(_, context): self.received = type(context) - dispatcher = Dispatcher(bot, Queue(), custom_context=CustomContext) + dispatcher = Dispatcher( + bot, Queue(), context_customizer=ContextCustomizer(context=CustomContext) + ) dispatcher.add_error_handler(error_handler) dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) @@ -825,7 +828,9 @@ def test_custom_context_handler_callback(self, bot): def callback(_, context): self.received = type(context) - dispatcher = Dispatcher(bot, Queue(), custom_context=CustomContext) + dispatcher = Dispatcher( + bot, Queue(), context_customizer=ContextCustomizer(context=CustomContext) + ) dispatcher.add_handler(MessageHandler(Filters.all, callback)) dispatcher.process_update(self.message_update) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 622980152a7..709d41e96a5 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -28,7 +28,7 @@ import pytz from apscheduler.schedulers import SchedulerNotRunningError from flaky import flaky -from telegram.ext import JobQueue, Updater, Job, CallbackContext, Dispatcher +from telegram.ext import JobQueue, Updater, Job, CallbackContext, Dispatcher, ContextCustomizer class CustomContext(CallbackContext): @@ -516,7 +516,9 @@ def test_dispatch_error_that_raises_errors(self, job_queue, dp, caplog): assert 'No error handlers are registered' in rec.getMessage() def test_custom_context(self, bot, job_queue): - dispatcher = Dispatcher(bot, Queue(), custom_context=CustomContext) + dispatcher = Dispatcher( + bot, Queue(), context_customizer=ContextCustomizer(context=CustomContext) + ) job_queue.set_dispatcher(dispatcher) def callback(context): diff --git a/tests/test_updater.py b/tests/test_updater.py index 5c20db4f01f..ad9353b665c 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -582,9 +582,8 @@ def test_mutual_exclude_use_context_dispatcher(self): def test_mutual_exclude_custom_context_dispatcher(self): dispatcher = Dispatcher(None, None) - custom_context = object with pytest.raises(ValueError): - Updater(dispatcher=dispatcher, custom_context=custom_context) + Updater(dispatcher=dispatcher, context_customizer=True) def test_defaults_warning(self, bot): with pytest.warns(TelegramDeprecationWarning, match='no effect when a Bot is passed'): From 9189994f3bdfaccb56368796763d04f541e1ec87 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 15 Jan 2021 17:03:53 +0100 Subject: [PATCH 05/31] Get typing to work to reasonable accuracy --- telegram/ext/basepersistence.py | 11 +- telegram/ext/contextcustomizer.py | 584 +++++++++++++++++++++++++++++- telegram/ext/dispatcher.py | 39 +- telegram/ext/handler.py | 8 +- telegram/ext/updater.py | 85 ++++- telegram/utils/types.py | 13 +- 6 files changed, 702 insertions(+), 38 deletions(-) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 385c19d2f0e..5a60994f805 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -19,8 +19,9 @@ """This module contains the BasePersistence class.""" import warnings from abc import ABC, abstractmethod +from collections.abc import Mapping from copy import copy -from typing import Any, DefaultDict, Dict, Optional, Tuple, cast, ClassVar, Generic, Mapping +from typing import Any, Dict, Optional, Tuple, cast, ClassVar, Generic from telegram import Bot @@ -84,10 +85,10 @@ def __new__(cls, *args: Any, **kwargs: Any) -> 'BasePersistence': # pylint: dis update_chat_data = instance.update_chat_data update_bot_data = instance.update_bot_data - def get_user_data_insert_bot() -> UDM[int, UD]: + def get_user_data_insert_bot() -> Mapping[int, UD]: return instance.insert_bot(get_user_data()) - def get_chat_data_insert_bot() -> CDM[int, CD]: + def get_chat_data_insert_bot() -> Mapping[int, CD]: return instance.insert_bot(get_chat_data()) def get_bot_data_insert_bot() -> BD: @@ -288,7 +289,7 @@ def _insert_bot(self, obj: object, memo: Dict[int, Any]) -> object: # pylint: d return obj @abstractmethod - def get_user_data(self) -> UDM[int, UD]: + def get_user_data(self) -> Mapping[int, UD]: """ "Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``user_data`` if stored, or an empty ``Mapping`` with integer keys. @@ -298,7 +299,7 @@ def get_user_data(self) -> UDM[int, UD]: """ @abstractmethod - def get_chat_data(self) -> CDM[int, CD]: + def get_chat_data(self) -> Mapping[int, CD]: """ "Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``chat_data`` if stored, or an empty ``Mapping`` with integer keys. diff --git a/telegram/ext/contextcustomizer.py b/telegram/ext/contextcustomizer.py index 0d8e5fb4281..f2bfd00cfef 100644 --- a/telegram/ext/contextcustomizer.py +++ b/telegram/ext/contextcustomizer.py @@ -16,9 +16,11 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=R0201 """This module contains the auxiliary class ContextCustomizer.""" -from collections import defaultdict, Mapping -from typing import Type, Optional, Any, NoReturn, Generic +from collections import defaultdict +from collections.abc import Mapping +from typing import Type, Optional, Any, NoReturn, Generic, overload from telegram.ext.callbackcontext import CallbackContext from telegram.utils.types import CCT, UD, CD, BD, CDM, UDM @@ -56,14 +58,580 @@ class ContextCustomizer(Generic[CCT, UD, CD, BD, UDM, CDM]): """ + # Overload signature were autogenerated with https://git.io/JtJPj + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, dict, defaultdict, defaultdict]", + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, dict, defaultdict, defaultdict]", + context: Type[CCT], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, dict, defaultdict, defaultdict]", + bot_data: Type[UD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, dict, defaultdict, defaultdict]", + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, BD, defaultdict, defaultdict]", + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, dict, UDM, defaultdict]", + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, dict, defaultdict, CDM]", + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, dict, defaultdict, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, dict, defaultdict, defaultdict]", + context: Type[CCT], + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, BD, defaultdict, defaultdict]", + context: Type[CCT], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, dict, UDM, defaultdict]", + context: Type[CCT], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, dict, defaultdict, CDM]", + context: Type[CCT], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, dict, defaultdict, defaultdict]", + bot_data: Type[UD], + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, BD, defaultdict, defaultdict]", + bot_data: Type[UD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, dict, UDM, defaultdict]", + bot_data: Type[UD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, dict, defaultdict, CDM]", + bot_data: Type[UD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, BD, defaultdict, defaultdict]", + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, dict, UDM, defaultdict]", + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, dict, defaultdict, CDM]", + chat_data: Type[CD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, BD, UDM, defaultdict]", + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, BD, defaultdict, CDM]", + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, dict, UDM, CDM]", + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, dict, defaultdict, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, BD, defaultdict, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, dict, UDM, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, dict, defaultdict, CDM]", + context: Type[CCT], + bot_data: Type[UD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, BD, defaultdict, defaultdict]", + context: Type[CCT], + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, dict, UDM, defaultdict]", + context: Type[CCT], + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, dict, defaultdict, CDM]", + context: Type[CCT], + chat_data: Type[CD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, BD, UDM, defaultdict]", + context: Type[CCT], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, BD, defaultdict, CDM]", + context: Type[CCT], + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, dict, UDM, CDM]", + context: Type[CCT], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, BD, defaultdict, defaultdict]", + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, dict, UDM, defaultdict]", + bot_data: Type[UD], + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, dict, defaultdict, CDM]", + bot_data: Type[UD], + chat_data: Type[CD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, BD, UDM, defaultdict]", + bot_data: Type[UD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, BD, defaultdict, CDM]", + bot_data: Type[UD], + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, dict, UDM, CDM]", + bot_data: Type[UD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, BD, UDM, defaultdict]", + chat_data: Type[CD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, BD, defaultdict, CDM]", + chat_data: Type[CD], + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, dict, UDM, CDM]", + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, dict, BD, UDM, CDM]", + user_data: Type[BD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, BD, defaultdict, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, dict, UDM, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, dict, defaultdict, CDM]", + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, BD, UDM, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, BD, defaultdict, CDM]", + context: Type[CCT], + bot_data: Type[UD], + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, dict, UDM, CDM]", + context: Type[CCT], + bot_data: Type[UD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, BD, UDM, defaultdict]", + context: Type[CCT], + chat_data: Type[CD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, BD, defaultdict, CDM]", + context: Type[CCT], + chat_data: Type[CD], + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, dict, UDM, CDM]", + context: Type[CCT], + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, dict, BD, UDM, CDM]", + context: Type[CCT], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, BD, UDM, defaultdict]", + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, BD, defaultdict, CDM]", + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, dict, UDM, CDM]", + bot_data: Type[UD], + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, dict, BD, UDM, CDM]", + bot_data: Type[UD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, dict, CD, BD, UDM, CDM]", + chat_data: Type[CD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, BD, UDM, defaultdict]", + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, BD, defaultdict, CDM]", + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, dict, UDM, CDM]", + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, dict, BD, UDM, CDM]", + context: Type[CCT], + bot_data: Type[UD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CCT, dict, CD, BD, UDM, CDM]", + context: Type[CCT], + chat_data: Type[CD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + @overload + def __init__( + self: "ContextCustomizer[CallbackContext, UD, CD, BD, UDM, CDM]", + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + + def __init__( # type: ignore[no-untyped-def] self, - context: Type[CCT] = CallbackContext, - bot_data: Type[BD] = dict, - chat_data: Type[CD] = dict, - user_data: Type[UD] = dict, - chat_data_mapping: Type[CDM] = defaultdict, - user_data_mapping: Type[UDM] = defaultdict, + context=CallbackContext, + bot_data=dict, + chat_data=dict, + user_data=dict, + chat_data_mapping=defaultdict, + user_data_mapping=defaultdict, ): if not issubclass(context, CallbackContext): raise ValueError('context must be a subclass of CallbackContext.') diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 1cc1a4cb63f..5c9c76c04ed 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -36,6 +36,7 @@ Union, Generic, TypeVar, + overload, ) from uuid import uuid4 @@ -49,6 +50,7 @@ from telegram.utils.types import CCT, UD, CD, BD, UDM, CDM if TYPE_CHECKING: + from collections import defaultdict from telegram import Bot from telegram.ext import JobQueue @@ -158,16 +160,45 @@ class Dispatcher(Generic[CCT, UD, CD, BD, UDM, CDM]): __singleton = None logger = logging.getLogger(__name__) + @overload def __init__( + self: 'Dispatcher[CallbackContext, dict, dict, dict, defaultdict, defaultdict]', + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True, + ): + ... + + @overload + def __init__( + self: 'Dispatcher[CCT, UD, CD, BD, UDM, CDM]', + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True, + context_customizer: ContextCustomizer[ + CCT, UD, CD, BD, UDM, CDM + ] = ContextCustomizer(), # type: ignore[assignment] + ): + ... + + def __init__( # type: ignore[no-untyped-def] self, bot: 'Bot', update_queue: Queue, workers: int = 4, exception_event: Event = None, job_queue: 'JobQueue' = None, - persistence: BasePersistence[UD, CD, BD, UDM, CDM] = None, + persistence: BasePersistence = None, use_context: bool = True, - context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = ContextCustomizer(), + context_customizer=ContextCustomizer(), ): self.bot = bot self.update_queue = update_queue @@ -643,7 +674,7 @@ def __update_persistence(self, update: Any = None) -> None: def add_error_handler( self, - callback: Callable[[Any, CCT], None], + callback: Callable[[object, CCT], None], run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621 ) -> None: """Registers an error handler in the Dispatcher. This handler will receive every error @@ -679,7 +710,7 @@ def add_error_handler( self.error_handlers[callback] = run_async - def remove_error_handler(self, callback: Callable[[Any, CCT], None]) -> None: + def remove_error_handler(self, callback: Callable[[object, CCT], None]) -> None: """Removes an error handler. Args: diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index ccdc4de5e15..5764ebaad2b 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -211,13 +211,9 @@ def collect_optional_args( optional_args['job_queue'] = dispatcher.job_queue if self.pass_user_data and isinstance(update, Update): user = update.effective_user - optional_args['user_data'] = dispatcher.user_data[ - user.id if user else None # type: ignore[index] - ] + optional_args['user_data'] = dispatcher.user_data[user.id if user else None] if self.pass_chat_data and isinstance(update, Update): chat = update.effective_chat - optional_args['chat_data'] = dispatcher.chat_data[ - chat.id if chat else None # type: ignore[index] - ] + optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None] return optional_args diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index baea2ea74d6..65d3f93923b 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -35,8 +35,8 @@ Tuple, Union, no_type_check, - Type, Generic, + overload, ) from telegram import Bot, TelegramError @@ -49,7 +49,8 @@ from telegram.utils.webhookhandler import WebhookAppClass, WebhookServer if TYPE_CHECKING: - from telegram.ext import BasePersistence, Defaults + from collections import defaultdict + from telegram.ext import BasePersistence, Defaults, CallbackContext class Updater(Generic[CCT, UD, CD, BD, UDM, CDM]): @@ -124,8 +125,9 @@ class Updater(Generic[CCT, UD, CD, BD, UDM, CDM]): _request = None + @overload def __init__( - self, + self: 'Updater[CallbackContext, dict, dict, dict, defaultdict, defaultdict]', token: str = None, base_url: str = None, workers: int = 4, @@ -134,10 +136,54 @@ def __init__( private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, - persistence: 'BasePersistence[UD, CD, BD, UDM, CDM]' = None, + persistence: BasePersistence = None, # pylint: disable=E0601 defaults: 'Defaults' = None, use_context: bool = True, + base_file_url: str = None, + ): + ... + + @overload + def __init__( + self: 'Updater[CCT, UD, CD, BD, UDM, CDM]', + token: str = None, + base_url: str = None, + workers: int = 4, + bot: Bot = None, + private_key: bytes = None, + private_key_password: bytes = None, + user_sig_handler: Callable = None, + request_kwargs: Dict[str, Any] = None, + persistence: BasePersistence = None, + defaults: 'Defaults' = None, + use_context: bool = True, + base_file_url: str = None, + context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, + ): + ... + + @overload + def __init__( + self: 'Updater[CCT, UD, CD, BD, UDM, CDM]', + user_sig_handler: Callable = None, dispatcher: Dispatcher[CCT, UD, CD, BD, UDM, CDM] = None, + ): + ... + + def __init__( # type: ignore[no-untyped-def,misc] + self, + token: str = None, + base_url: str = None, + workers: int = 4, + bot: Bot = None, + private_key: bytes = None, + private_key_password: bytes = None, + user_sig_handler: Callable = None, + request_kwargs: Dict[str, Any] = None, + persistence: BasePersistence = None, + defaults: 'Defaults' = None, + use_context: bool = True, + dispatcher=None, base_file_url: str = None, context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, ): @@ -206,16 +252,27 @@ def __init__( self.job_queue = JobQueue() self.__exception_event = Event() self.persistence = persistence - self.dispatcher: Dispatcher[CCT] = Dispatcher( - self.bot, - self.update_queue, - job_queue=self.job_queue, - workers=workers, - exception_event=self.__exception_event, - persistence=persistence, - use_context=use_context, - context_customizer=context_customizer or ContextCustomizer(), - ) + if context_customizer: + self.dispatcher = Dispatcher( + self.bot, + self.update_queue, + job_queue=self.job_queue, + workers=workers, + exception_event=self.__exception_event, + persistence=persistence, + use_context=use_context, + context_customizer=context_customizer, + ) + else: + self.dispatcher = Dispatcher( # type: ignore[assignment] + self.bot, + self.update_queue, + job_queue=self.job_queue, + workers=workers, + exception_event=self.__exception_event, + persistence=persistence, + use_context=use_context, + ) self.job_queue.set_dispatcher(self.dispatcher) else: con_pool_size = dispatcher.workers + 4 diff --git a/telegram/utils/types.py b/telegram/utils/types.py index 226111f745c..7dd31211889 100644 --- a/telegram/utils/types.py +++ b/telegram/utils/types.py @@ -18,7 +18,18 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains custom typing aliases.""" from pathlib import Path -from typing import IO, TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar, Union, Mapping +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Tuple, + TypeVar, + Union, + Mapping, +) if TYPE_CHECKING: from telegram import InputFile From 254fe2def5a53eb5d433d2719e24ef7f879a8d9b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 16 Jan 2021 16:31:26 +0100 Subject: [PATCH 06/31] Improve type hints some more --- telegram/ext/contextcustomizer.py | 289 ++++++++++++++++-------------- telegram/ext/dispatcher.py | 20 ++- telegram/utils/types.py | 5 + 3 files changed, 167 insertions(+), 147 deletions(-) diff --git a/telegram/ext/contextcustomizer.py b/telegram/ext/contextcustomizer.py index f2bfd00cfef..98c5e771092 100644 --- a/telegram/ext/contextcustomizer.py +++ b/telegram/ext/contextcustomizer.py @@ -20,10 +20,10 @@ """This module contains the auxiliary class ContextCustomizer.""" from collections import defaultdict from collections.abc import Mapping -from typing import Type, Optional, Any, NoReturn, Generic, overload +from typing import Type, Any, NoReturn, Generic, overload, Dict # pylint: disable=W0611 from telegram.ext.callbackcontext import CallbackContext -from telegram.utils.types import CCT, UD, CD, BD, CDM, UDM +from telegram.utils.types import CCT, UD, CD, BD, CDM, UDM, IntDD # pylint: disable=W0611 class ContextCustomizer(Generic[CCT, UD, CD, BD, UDM, CDM]): @@ -36,12 +36,12 @@ class ContextCustomizer(Generic[CCT, UD, CD, BD, UDM, CDM]): :class:`telegram.ext.CallbackContext`. Defaults to :class:`telegram.ext.CallbackContext`. bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + (error-)handler callbacks and job callbacks. Defaults to :obj:`Dict`. Must support instantiating without arguments chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + (error-)handler callbacks and job callbacks. Defaults to :obj:`Dict`. user_data (:obj:`type`, optional): Determines the type of ``context.user_data` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + (error-)handler callbacks and job callbacks. Defaults to :obj:`Dict`. chat_data_mapping (:obj:`type`, optional): In combination with :attr:`chat_data` determines the type of :attr:`telegram.ext.Dispatcher.chat_data`. Must support instantiating via @@ -58,67 +58,71 @@ class ContextCustomizer(Generic[CCT, UD, CD, BD, UDM, CDM]): """ - # Overload signature were autogenerated with https://git.io/JtJPj - @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, dict, defaultdict, defaultdict]", + self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, " + "IntDD[Dict], IntDD[Dict]]", ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, dict, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, Dict, Dict, Dict, IntDD[Dict], IntDD[Dict]]", context: Type[CCT], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, dict, defaultdict, defaultdict]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, " + "IntDD[UD], IntDD[Dict]]", + bot_data: Type[BD], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, dict, defaultdict, defaultdict]", + self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, " + "IntDD[Dict], IntDD[CD]]", chat_data: Type[CD], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, BD, defaultdict, defaultdict]", - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, " + "IntDD[Dict], IntDD[Dict]]", + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, dict, UDM, defaultdict]", + self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, " + "Dict, UDM, IntDD[Dict]]", chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, dict, defaultdict, CDM]", + self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, " + "IntDD[Dict], CDM]", user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, dict, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, UD, Dict, Dict, IntDD[UD], IntDD[Dict]]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, dict, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, Dict, CD, Dict, IntDD[Dict], IntDD[CD]]", context: Type[CCT], chat_data: Type[CD], ): @@ -126,15 +130,15 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, BD, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, Dict, Dict, BD, IntDD[Dict], IntDD[Dict]]", context: Type[CCT], - user_data: Type[BD], + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, dict, UDM, defaultdict]", + self: "ContextCustomizer[CCT, Dict, Dict, Dict, UDM, IntDD[Dict]]", context: Type[CCT], chat_data_mapping: Type[UDM], ): @@ -142,7 +146,7 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, dict, defaultdict, CDM]", + self: "ContextCustomizer[CCT, Dict, Dict, Dict, IntDD[Dict], CDM]", context: Type[CCT], user_data_mapping: Type[CDM], ): @@ -150,47 +154,53 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, dict, defaultdict, defaultdict]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, " + "IntDD[UD], IntDD[CD]]", + bot_data: Type[BD], chat_data: Type[CD], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, BD, defaultdict, defaultdict]", - bot_data: Type[UD], - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, " + "IntDD[UD], IntDD[Dict]]", + bot_data: Type[BD], + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, dict, UDM, defaultdict]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, " + "UDM, IntDD[Dict]]", + bot_data: Type[BD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, dict, defaultdict, CDM]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, " + "IntDD[UD], CDM]", + bot_data: Type[BD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, BD, defaultdict, defaultdict]", + self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, " + "IntDD[Dict], IntDD[CD]]", chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, dict, UDM, defaultdict]", + self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, " + "UDM, IntDD[CD]]", chat_data: Type[CD], chat_data_mapping: Type[UDM], ): @@ -198,7 +208,8 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, dict, defaultdict, CDM]", + self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, " + "IntDD[Dict], CDM]", chat_data: Type[CD], user_data_mapping: Type[CDM], ): @@ -206,23 +217,25 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, BD, UDM, defaultdict]", - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, UDM, " + "IntDD[Dict]]", + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, BD, defaultdict, CDM]", - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, " + "IntDD[Dict], CDM]", + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, dict, UDM, CDM]", + self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, UDM, CDM]", chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -230,52 +243,52 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, UD, CD, dict, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, UD, CD, Dict, IntDD[UD], IntDD[CD]]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data: Type[CD], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, BD, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, UD, Dict, BD, IntDD[UD], IntDD[Dict]]", context: Type[CCT], - bot_data: Type[UD], - user_data: Type[BD], + bot_data: Type[BD], + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, dict, UDM, defaultdict]", + self: "ContextCustomizer[CCT, UD, Dict, Dict, UDM, IntDD[Dict]]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, dict, defaultdict, CDM]", + self: "ContextCustomizer[CCT, UD, Dict, Dict, IntDD[UD], CDM]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, BD, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, Dict, CD, BD, IntDD[Dict], IntDD[CD]]", context: Type[CCT], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, dict, UDM, defaultdict]", + self: "ContextCustomizer[CCT, Dict, CD, Dict, UDM, IntDD[CD]]", context: Type[CCT], chat_data: Type[CD], chat_data_mapping: Type[UDM], @@ -284,7 +297,7 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, dict, defaultdict, CDM]", + self: "ContextCustomizer[CCT, Dict, CD, Dict, IntDD[Dict], CDM]", context: Type[CCT], chat_data: Type[CD], user_data_mapping: Type[CDM], @@ -293,25 +306,25 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, BD, UDM, defaultdict]", + self: "ContextCustomizer[CCT, Dict, Dict, BD, UDM, IntDD[Dict]]", context: Type[CCT], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, BD, defaultdict, CDM]", + self: "ContextCustomizer[CCT, Dict, Dict, BD, IntDD[Dict], CDM]", context: Type[CCT], - user_data: Type[BD], + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, dict, UDM, CDM]", + self: "ContextCustomizer[CCT, Dict, Dict, Dict, UDM, CDM]", context: Type[CCT], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], @@ -320,17 +333,17 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, BD, defaultdict, defaultdict]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, IntDD[UD], IntDD[CD]]", + bot_data: Type[BD], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, dict, UDM, defaultdict]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, UDM, IntDD[CD]]", + bot_data: Type[BD], chat_data: Type[CD], chat_data_mapping: Type[UDM], ): @@ -338,8 +351,8 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, dict, defaultdict, CDM]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, IntDD[UD], CDM]", + bot_data: Type[BD], chat_data: Type[CD], user_data_mapping: Type[CDM], ): @@ -347,26 +360,26 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, BD, UDM, defaultdict]", - bot_data: Type[UD], - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, UDM, IntDD[Dict]]", + bot_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, BD, defaultdict, CDM]", - bot_data: Type[UD], - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, IntDD[UD], CDM]", + bot_data: Type[BD], + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, dict, UDM, CDM]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, UDM, CDM]", + bot_data: Type[BD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -374,25 +387,25 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, BD, UDM, defaultdict]", + self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, UDM, IntDD[CD]]", chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, BD, defaultdict, CDM]", + self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, IntDD[Dict], CDM]", chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, dict, UDM, CDM]", + self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, UDM, CDM]", chat_data: Type[CD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], @@ -401,8 +414,8 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, dict, BD, UDM, CDM]", - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, UDM, CDM]", + user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -410,19 +423,19 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, UD, CD, BD, defaultdict, defaultdict]", + self: "ContextCustomizer[CCT, UD, CD, BD, IntDD[UD], IntDD[CD]]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, CD, dict, UDM, defaultdict]", + self: "ContextCustomizer[CCT, UD, CD, Dict, UDM, IntDD[CD]]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data: Type[CD], chat_data_mapping: Type[UDM], ): @@ -430,9 +443,9 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, UD, CD, dict, defaultdict, CDM]", + self: "ContextCustomizer[CCT, UD, CD, Dict, IntDD[UD], CDM]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data: Type[CD], user_data_mapping: Type[CDM], ): @@ -440,29 +453,29 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, BD, UDM, defaultdict]", + self: "ContextCustomizer[CCT, UD, Dict, BD, UDM, IntDD[Dict]]", context: Type[CCT], - bot_data: Type[UD], - user_data: Type[BD], + bot_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, BD, defaultdict, CDM]", + self: "ContextCustomizer[CCT, UD, Dict, BD, IntDD[UD], CDM]", context: Type[CCT], - bot_data: Type[UD], - user_data: Type[BD], + bot_data: Type[BD], + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, dict, UDM, CDM]", + self: "ContextCustomizer[CCT, UD, Dict, Dict, UDM, CDM]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -470,27 +483,27 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, BD, UDM, defaultdict]", + self: "ContextCustomizer[CCT, Dict, CD, BD, UDM, IntDD[CD]]", context: Type[CCT], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, BD, defaultdict, CDM]", + self: "ContextCustomizer[CCT, Dict, CD, BD, IntDD[Dict], CDM]", context: Type[CCT], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, dict, UDM, CDM]", + self: "ContextCustomizer[CCT, Dict, CD, Dict, UDM, CDM]", context: Type[CCT], chat_data: Type[CD], chat_data_mapping: Type[UDM], @@ -500,9 +513,9 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, dict, dict, BD, UDM, CDM]", + self: "ContextCustomizer[CCT, Dict, Dict, BD, UDM, CDM]", context: Type[CCT], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -510,28 +523,28 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, BD, UDM, defaultdict]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, UDM, IntDD[CD]]", + bot_data: Type[BD], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, BD, defaultdict, CDM]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, IntDD[UD], CDM]", + bot_data: Type[BD], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, dict, UDM, CDM]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, UDM, CDM]", + bot_data: Type[BD], chat_data: Type[CD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], @@ -540,9 +553,9 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, dict, BD, UDM, CDM]", - bot_data: Type[UD], - user_data: Type[BD], + self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, UDM, CDM]", + bot_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -550,9 +563,9 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, dict, CD, BD, UDM, CDM]", + self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, UDM, CDM]", chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -560,31 +573,31 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, UD, CD, BD, UDM, defaultdict]", + self: "ContextCustomizer[CCT, UD, CD, BD, UDM, IntDD[CD]]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, CD, BD, defaultdict, CDM]", + self: "ContextCustomizer[CCT, UD, CD, BD, IntDD[UD], CDM]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], user_data_mapping: Type[CDM], ): ... @overload def __init__( - self: "ContextCustomizer[CCT, UD, CD, dict, UDM, CDM]", + self: "ContextCustomizer[CCT, UD, CD, Dict, UDM, CDM]", context: Type[CCT], - bot_data: Type[UD], + bot_data: Type[BD], chat_data: Type[CD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], @@ -593,10 +606,10 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, UD, dict, BD, UDM, CDM]", + self: "ContextCustomizer[CCT, UD, Dict, BD, UDM, CDM]", context: Type[CCT], - bot_data: Type[UD], - user_data: Type[BD], + bot_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -604,10 +617,10 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CCT, dict, CD, BD, UDM, CDM]", + self: "ContextCustomizer[CCT, Dict, CD, BD, UDM, CDM]", context: Type[CCT], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -615,10 +628,10 @@ def __init__( @overload def __init__( - self: "ContextCustomizer[CallbackContext, UD, CD, BD, UDM, CDM]", - bot_data: Type[UD], + self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, UDM, CDM]", + bot_data: Type[BD], chat_data: Type[CD], - user_data: Type[BD], + user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM], ): @@ -648,7 +661,7 @@ def __init__( # type: ignore[no-untyped-def] self._user_data_mapping = user_data_mapping @property - def context(self) -> Optional[Type]: + def context(self) -> Type[CCT]: return self._context @context.setter @@ -656,7 +669,7 @@ def context(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") @property - def bot_data(self) -> Optional[Type]: + def bot_data(self) -> Type[BD]: return self._bot_data @bot_data.setter @@ -664,7 +677,7 @@ def bot_data(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") @property - def chat_data(self) -> Optional[Type]: + def chat_data(self) -> Type[CD]: return self._chat_data @chat_data.setter @@ -672,7 +685,7 @@ def chat_data(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") @property - def user_data(self) -> Optional[Type]: + def user_data(self) -> Type[UD]: return self._user_data @user_data.setter @@ -680,7 +693,7 @@ def user_data(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") @property - def user_data_mapping(self) -> Optional[Type]: + def user_data_mapping(self) -> Type[UDM]: return self._user_data_mapping @user_data_mapping.setter @@ -688,7 +701,7 @@ def user_data_mapping(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") @property - def chat_data_mapping(self) -> Optional[Type]: + def chat_data_mapping(self) -> Type[CDM]: return self._chat_data_mapping @chat_data_mapping.setter diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 5c9c76c04ed..4effde2a989 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -47,10 +47,9 @@ from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE -from telegram.utils.types import CCT, UD, CD, BD, UDM, CDM +from telegram.utils.types import CCT, UD, CD, BD, UDM, CDM, IntDD # pylint: disable=W0611 if TYPE_CHECKING: - from collections import defaultdict from telegram import Bot from telegram.ext import JobQueue @@ -162,7 +161,8 @@ class Dispatcher(Generic[CCT, UD, CD, BD, UDM, CDM]): @overload def __init__( - self: 'Dispatcher[CallbackContext, dict, dict, dict, defaultdict, defaultdict]', + self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, ' + 'IntDD[Dict], IntDD[Dict]]', bot: 'Bot', update_queue: Queue, workers: int = 4, @@ -189,7 +189,7 @@ def __init__( ): ... - def __init__( # type: ignore[no-untyped-def] + def __init__( self, bot: 'Bot', update_queue: Queue, @@ -198,7 +198,9 @@ def __init__( # type: ignore[no-untyped-def] job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, - context_customizer=ContextCustomizer(), + context_customizer: ContextCustomizer[ + CCT, UD, CD, BD, UDM, CDM + ] = ContextCustomizer(), # type: ignore[assignment] ): self.bot = bot self.update_queue = update_queue @@ -214,10 +216,10 @@ def __init__( # type: ignore[no-untyped-def] stacklevel=3, ) - self.user_data = self.context_customizer.user_data_mapping( + self.user_data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] self.context_customizer.user_data ) - self.chat_data = self.context_customizer.chat_data_mapping( + self.chat_data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] self.context_customizer.chat_data ) self.bot_data = self.context_customizer.bot_data() @@ -229,14 +231,14 @@ def __init__( # type: ignore[no-untyped-def] self.persistence = persistence self.persistence.set_bot(self.bot) if self.persistence.store_user_data: - self.user_data = self.persistence.get_user_data() + self.user_data = self.persistence.get_user_data() # type: ignore[assignment] if not isinstance(self.user_data, self.context_customizer.user_data_mapping): raise ValueError( f"user_data must be of type " f"{self.context_customizer.user_data_mapping.__name__}" ) if self.persistence.store_chat_data: - self.chat_data = self.persistence.get_chat_data() + self.chat_data = self.persistence.get_chat_data() # type: ignore[assignment] if not isinstance(self.chat_data, self.context_customizer.chat_data_mapping): raise ValueError( f"chat_data must be of type " diff --git a/telegram/utils/types.py b/telegram/utils/types.py index 7dd31211889..4ca70feb12d 100644 --- a/telegram/utils/types.py +++ b/telegram/utils/types.py @@ -29,6 +29,7 @@ TypeVar, Union, Mapping, + DefaultDict, ) if TYPE_CHECKING: @@ -65,3 +66,7 @@ """Type of the user data mapping.""" CDM = TypeVar('CDM', bound=Mapping) """Type of the chat data mapping.""" + +DDType = TypeVar('DDType') +IntDD = DefaultDict[int, DDType] +"""Type for default dicts with integer keys and generic value.""" From f24cdff56d3c0683f4f03e95d4677f2d858f0965 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 16 Jan 2021 17:10:00 +0100 Subject: [PATCH 07/31] Test Dispatcher integration --- examples/echobot.py | 2 +- telegram/ext/basepersistence.py | 3 +- telegram/ext/contextcustomizer.py | 12 +++++++ telegram/ext/updater.py | 6 ++-- tests/test_dispatcher.py | 54 +++++++++++++++++++++++++++---- tests/test_jobqueue.py | 15 +++++++-- 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/examples/echobot.py b/examples/echobot.py index bb9ceb855bd..c8a264584bd 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -42,7 +42,7 @@ def help_command(update: Update, context: CallbackContext) -> None: update.message.reply_text('Help!') -def echo(update: str, context: CallbackContext) -> None: +def echo(update: Update, context: CallbackContext) -> None: """Echo the user message.""" update.message.reply_text(update.message.text) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 5a60994f805..ec52fd9ced0 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -19,9 +19,8 @@ """This module contains the BasePersistence class.""" import warnings from abc import ABC, abstractmethod -from collections.abc import Mapping from copy import copy -from typing import Any, Dict, Optional, Tuple, cast, ClassVar, Generic +from typing import Any, Dict, Optional, Tuple, cast, ClassVar, Generic, Mapping from telegram import Bot diff --git a/telegram/ext/contextcustomizer.py b/telegram/ext/contextcustomizer.py index 98c5e771092..961d32706aa 100644 --- a/telegram/ext/contextcustomizer.py +++ b/telegram/ext/contextcustomizer.py @@ -637,6 +637,18 @@ def __init__( ): ... + @overload + def __init__( + self: "ContextCustomizer[CCT, UD, CD, BD, UDM, CDM]", + context: Type[CCT], + bot_data: Type[BD], + chat_data: Type[CD], + user_data: Type[UD], + chat_data_mapping: Type[UDM], + user_data_mapping: Type[CDM], + ): + ... + def __init__( # type: ignore[no-untyped-def] self, context=CallbackContext, diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 65d3f93923b..62186530f54 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -136,7 +136,7 @@ def __init__( private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, - persistence: BasePersistence = None, # pylint: disable=E0601 + persistence: 'BasePersistence' = None, # pylint: disable=E0601 defaults: 'Defaults' = None, use_context: bool = True, base_file_url: str = None, @@ -154,7 +154,7 @@ def __init__( private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, - persistence: BasePersistence = None, + persistence: 'BasePersistence' = None, defaults: 'Defaults' = None, use_context: bool = True, base_file_url: str = None, @@ -180,7 +180,7 @@ def __init__( # type: ignore[no-untyped-def,misc] private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, - persistence: BasePersistence = None, + persistence: 'BasePersistence' = None, defaults: 'Defaults' = None, use_context: bool = True, dispatcher=None, diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 4fb4007300c..4c001b11c5b 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -904,29 +904,71 @@ def dummy_callback(*args, **kwargs): finally: dp.bot.defaults = None + def test_custom_context_init(self, bot): + class CustomUserMapping(defaultdict): + pass + + class CustomChatMapping(defaultdict): + pass + + cc = ContextCustomizer( + context=CustomContext, + user_data=int, + chat_data=float, + bot_data=complex, + user_data_mapping=CustomUserMapping, + chat_data_mapping=CustomChatMapping, + ) + + dispatcher = Dispatcher(bot, Queue(), context_customizer=cc) + + assert isinstance(dispatcher.user_data, CustomUserMapping) + assert isinstance(dispatcher.user_data[1], int) + assert isinstance(dispatcher.chat_data, CustomChatMapping) + assert isinstance(dispatcher.chat_data[1], float) + assert isinstance(dispatcher.bot_data, complex) + def test_custom_context_error_handler(self, bot): def error_handler(_, context): - self.received = type(context) + self.received = ( + type(context), + type(context.user_data), + type(context.chat_data), + type(context.bot_data), + ) dispatcher = Dispatcher( - bot, Queue(), context_customizer=ContextCustomizer(context=CustomContext) + bot, + Queue(), + context_customizer=ContextCustomizer( + context=CustomContext, bot_data=int, user_data=float, chat_data=complex + ), ) dispatcher.add_error_handler(error_handler) dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) dispatcher.process_update(self.message_update) sleep(0.1) - assert self.received == CustomContext + assert self.received == (CustomContext, float, complex, int) def test_custom_context_handler_callback(self, bot): def callback(_, context): - self.received = type(context) + self.received = ( + type(context), + type(context.user_data), + type(context.chat_data), + type(context.bot_data), + ) dispatcher = Dispatcher( - bot, Queue(), context_customizer=ContextCustomizer(context=CustomContext) + bot, + Queue(), + context_customizer=ContextCustomizer( + context=CustomContext, bot_data=int, user_data=float, chat_data=complex + ), ) dispatcher.add_handler(MessageHandler(Filters.all, callback)) dispatcher.process_update(self.message_update) sleep(0.1) - assert self.received == CustomContext + assert self.received == (CustomContext, float, complex, int) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 3813db16777..a665672c208 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -518,13 +518,22 @@ def test_dispatch_error_that_raises_errors(self, job_queue, dp, caplog): def test_custom_context(self, bot, job_queue): dispatcher = Dispatcher( - bot, Queue(), context_customizer=ContextCustomizer(context=CustomContext) + bot, + Queue(), + context_customizer=ContextCustomizer( + context=CustomContext, bot_data=int, user_data=float, chat_data=complex + ), ) job_queue.set_dispatcher(dispatcher) def callback(context): - self.result = type(context) + self.result = ( + type(context), + context.user_data, + context.chat_data, + type(context.bot_data), + ) job_queue.run_once(callback, 0.1) sleep(0.15) - assert self.result == CustomContext + assert self.result == (CustomContext, None, None, int) From c2219445e47fd4c1e685e39970c21830a43631bf Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 17 Jan 2021 22:20:35 +0100 Subject: [PATCH 08/31] Pass CC to PP --- .../source/telegram.ext.contextcustomizer.rst | 5 +- telegram/ext/basepersistence.py | 2 +- telegram/ext/contextcustomizer.py | 24 +-- telegram/ext/dispatcher.py | 13 +- telegram/ext/handler.py | 4 +- telegram/ext/picklepersistence.py | 138 ++++++++++++++---- telegram/ext/updater.py | 31 ++-- 7 files changed, 147 insertions(+), 70 deletions(-) diff --git a/docs/source/telegram.ext.contextcustomizer.rst b/docs/source/telegram.ext.contextcustomizer.rst index 505c2c0c1c7..c08954f2d05 100644 --- a/docs/source/telegram.ext.contextcustomizer.rst +++ b/docs/source/telegram.ext.contextcustomizer.rst @@ -1,6 +1,9 @@ telegram.ext.ContextCustomizer ============================== -.. autoclass:: telegram.ext.ContextCustomizer +.. + We manually set the signature here in order to not display all the overload variants + +.. autoclass:: telegram.ext.ContextCustomizer(context: Type[CCT], bot_data: Type[BD], chat_data: Type[CD], user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM]) :members: :show-inheritance: diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index ec52fd9ced0..1ceaf58599e 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -27,7 +27,7 @@ from telegram.utils.types import ConversationDict, CD, UD, BD, UDM, CDM -class BasePersistence(ABC, Generic[UD, CD, BD, UDM, CDM]): +class BasePersistence(Generic[UD, CD, BD, UDM, CDM], ABC): """Interface class for adding persistence to your bot. Subclass this object for different implementations of a persistent bot. diff --git a/telegram/ext/contextcustomizer.py b/telegram/ext/contextcustomizer.py index 961d32706aa..880a75e2f89 100644 --- a/telegram/ext/contextcustomizer.py +++ b/telegram/ext/contextcustomizer.py @@ -35,27 +35,33 @@ class ContextCustomizer(Generic[CCT, UD, CD, BD, UDM, CDM]): (error-)handler callbacks and job callbacks. Must be a subclass of :class:`telegram.ext.CallbackContext`. Defaults to :class:`telegram.ext.CallbackContext`. - bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`Dict`. Must support - instantiating without arguments - chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`Dict`. - user_data (:obj:`type`, optional): Determines the type of ``context.user_data` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`Dict`. + bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments. + chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. chat_data_mapping (:obj:`type`, optional): In combination with :attr:`chat_data` determines - the type of :attr:`telegram.ext.Dispatcher.chat_data`. Must support instantiating via + the type of :attr:`telegram.ext.Dispatcher.chat_data`. Must be a subclass of + :obj:`collections.abc.Mapping` with integer keys supporting instantiating via .. code:: python chat_data_mapping(chat_data) + Defaults to :obj:`defaultdict`. + user_data_mapping (:obj:`type`, optional): In combination with :attr:`user_data` determines - the type of :attr:`telegram.ext.Dispatcher.user_data`. Must support instantiating via + the type of :attr:`telegram.ext.Dispatcher.user_data`. Must be a subclass of + :obj:`collections.abc.Mapping` with integer keys supporting instantiating via .. code:: python user_data_mapping(user_data) + Defaults to :obj:`defaultdict`. + """ @overload diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 4effde2a989..91feefc1200 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -37,6 +37,7 @@ Generic, TypeVar, overload, + cast, ) from uuid import uuid4 @@ -183,9 +184,7 @@ def __init__( job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, - context_customizer: ContextCustomizer[ - CCT, UD, CD, BD, UDM, CDM - ] = ContextCustomizer(), # type: ignore[assignment] + context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, ): ... @@ -198,16 +197,16 @@ def __init__( job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, - context_customizer: ContextCustomizer[ - CCT, UD, CD, BD, UDM, CDM - ] = ContextCustomizer(), # type: ignore[assignment] + context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, ): self.bot = bot self.update_queue = update_queue self.job_queue = job_queue self.workers = workers self.use_context = use_context - self.context_customizer = context_customizer + self.context_customizer = cast( + ContextCustomizer[CCT, UD, CD, BD, UDM, CDM], context_customizer or ContextCustomizer() + ) if not use_context: warnings.warn( diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index 5764ebaad2b..10238cbadea 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -211,9 +211,9 @@ def collect_optional_args( optional_args['job_queue'] = dispatcher.job_queue if self.pass_user_data and isinstance(update, Update): user = update.effective_user - optional_args['user_data'] = dispatcher.user_data[user.id if user else None] + optional_args['user_data'] = dispatcher.user_data[user.id] if user else None if self.pass_chat_data and isinstance(update, Update): chat = update.effective_chat - optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None] + optional_args['chat_data'] = dispatcher.chat_data[chat.id] if chat else None return optional_args diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index df6e7598ff4..36092e9a0b9 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -18,15 +18,17 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the PicklePersistence class.""" import pickle -from collections import defaultdict -from copy import deepcopy -from typing import Any, DefaultDict, Dict, Optional, Tuple +from typing import Any, Dict, Optional, Tuple, overload, Mapping, cast, TypeVar, MutableMapping from telegram.ext import BasePersistence -from telegram.utils.types import ConversationDict +from telegram.utils.types import ConversationDict, CD, UD, BD, IntDD # pylint: disable=W0611 +from .contextcustomizer import ContextCustomizer +UDM = TypeVar('UDM', bound=MutableMapping) +CDM = TypeVar('CDM', bound=MutableMapping) -class PicklePersistence(BasePersistence): + +class PicklePersistence(BasePersistence[UD, CD, BD, UDM, CDM]): """Using python's builtin pickle for making you bot persistent. Warning: @@ -54,6 +56,23 @@ class PicklePersistence(BasePersistence): :meth:`flush` is called and keep data in memory until that happens. When :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. + context_customizer (:class:`telegram.ext.ContextCustomizer`, optional): Pass an instance + of :class:`telegram.ext.ContextCustomizer` to customize the the types used in the + ``context`` interface. + + Note: + The types for :attr:`telegram.ext.ContextCustomizer.user_data_mapping` and + :attr:`telegram.ext.ContextCustomizer.chat_data_mapping` must be subclasses of + :class:`collections.abc.MutableMapping` and support instantiation via + + .. code:: python + + chat/user_data_mapping(chat/user_data_type[, data]) + + where ``data`` is of type ``chat/user_data_mapping``. + + If not passed, the defaults documented in + :class:`telegram.ext.ContextCustomizer` will be used. Attributes: filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` @@ -71,8 +90,35 @@ class PicklePersistence(BasePersistence): :meth:`flush` is called and keep data in memory until that happens. When :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. + context_customizer (:class:`telegram.ext.ContextCustomizer`): Container for the types used + in the ``context`` interface. """ + @overload + def __init__( + self: 'PicklePersistence[Dict, Dict, Dict, IntDD[Dict], IntDD[Dict]]', + filename: str, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + single_file: bool = True, + on_flush: bool = False, + ): + ... + + @overload + def __init__( + self: 'PicklePersistence[UD, CD, BD, UDM, CDM]', + filename: str, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + single_file: bool = True, + on_flush: bool = False, + context_customizer: ContextCustomizer[Any, UD, CD, BD, UDM, CDM] = None, + ): + ... + def __init__( self, filename: str, @@ -81,6 +127,7 @@ def __init__( store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, + context_customizer: ContextCustomizer[Any, UD, CD, BD, UDM, CDM] = None, ): super().__init__( store_user_data=store_user_data, @@ -90,26 +137,41 @@ def __init__( self.filename = filename self.single_file = single_file self.on_flush = on_flush - self.user_data: Optional[DefaultDict[int, Dict]] = None - self.chat_data: Optional[DefaultDict[int, Dict]] = None - self.bot_data: Optional[Dict] = None + self.user_data: Optional[MutableMapping[int, UD]] = None + self.chat_data: Optional[MutableMapping[int, CD]] = None + self.bot_data: Optional[BD] = None self.conversations: Optional[Dict[str, Dict[Tuple, Any]]] = None + self.context_customizer = cast( + ContextCustomizer[Any, UD, CD, BD, UDM, CDM], context_customizer or ContextCustomizer() + ) def load_singlefile(self) -> None: try: filename = self.filename with open(self.filename, "rb") as file: data = pickle.load(file) - self.user_data = defaultdict(dict, data['user_data']) - self.chat_data = defaultdict(dict, data['chat_data']) + self.user_data = ( + self.context_customizer.user_data_mapping( # type: ignore[call-arg] + self.context_customizer.user_data, data['user_data'] + ) + ) + self.chat_data = ( + self.context_customizer.chat_data_mapping( # type: ignore[call-arg] + self.context_customizer.chat_data, data['chat_data'] + ) + ) # For backwards compatibility with files not containing bot data self.bot_data = data.get('bot_data', {}) self.conversations = data['conversations'] except IOError: self.conversations = dict() - self.user_data = defaultdict(dict) - self.chat_data = defaultdict(dict) - self.bot_data = {} + self.user_data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] + self.context_customizer.user_data + ) + self.chat_data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] + self.context_customizer.chat_data + ) + self.bot_data = self.context_customizer.bot_data() except pickle.UnpicklingError as exc: raise TypeError(f"File {filename} does not contain valid pickle data") from exc except Exception as exc: @@ -142,7 +204,7 @@ def dump_file(filename: str, data: Any) -> None: with open(filename, "wb") as file: pickle.dump(data, file) - def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]: + def get_user_data(self) -> Mapping[int, UD]: """Returns the user_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: @@ -154,15 +216,19 @@ def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]: filename = f"{self.filename}_user_data" data = self.load_file(filename) if not data: - data = defaultdict(dict) + data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] + self.context_customizer.user_data + ) else: - data = defaultdict(dict, data) + data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] + self.context_customizer.user_data, data + ) self.user_data = data else: self.load_singlefile() - return deepcopy(self.user_data) # type: ignore[arg-type] + return self.user_data # type: ignore[return-value] - def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]: + def get_chat_data(self) -> Mapping[int, CD]: """Returns the chat_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: @@ -174,15 +240,19 @@ def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]: filename = f"{self.filename}_chat_data" data = self.load_file(filename) if not data: - data = defaultdict(dict) + data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] + self.context_customizer.chat_data + ) else: - data = defaultdict(dict, data) + data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] + self.context_customizer.chat_data, data + ) self.chat_data = data else: self.load_singlefile() - return deepcopy(self.chat_data) # type: ignore[arg-type] + return self.chat_data # type: ignore[return-value] - def get_bot_data(self) -> Dict[Any, Any]: + def get_bot_data(self) -> BD: """Returns the bot_data from the pickle file if it exists or an empty :obj:`dict`. Returns: @@ -198,7 +268,7 @@ def get_bot_data(self) -> Dict[Any, Any]: self.bot_data = data else: self.load_singlefile() - return deepcopy(self.bot_data) # type: ignore[arg-type] + return self.bot_data # type: ignore[return-value] def get_conversations(self, name: str) -> ConversationDict: """Returns the conversations from the pickle file if it exsists or an empty dict. @@ -244,7 +314,7 @@ def update_conversation( else: self.dump_singlefile() - def update_user_data(self, user_id: int, data: Dict) -> None: + def update_user_data(self, user_id: int, data: UD) -> None: """Will update the user_data and depending on :attr:`on_flush` save the pickle file. Args: @@ -252,7 +322,12 @@ def update_user_data(self, user_id: int, data: Dict) -> None: data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. """ if self.user_data is None: - self.user_data = defaultdict(dict) + self.user_data = cast( + MutableMapping[int, UD], + self.context_customizer.user_data_mapping( # type: ignore[call-arg] + self.context_customizer.user_data + ), + ) if self.user_data.get(user_id) == data: return self.user_data[user_id] = data @@ -263,7 +338,7 @@ def update_user_data(self, user_id: int, data: Dict) -> None: else: self.dump_singlefile() - def update_chat_data(self, chat_id: int, data: Dict) -> None: + def update_chat_data(self, chat_id: int, data: CD) -> None: """Will update the chat_data and depending on :attr:`on_flush` save the pickle file. Args: @@ -271,7 +346,12 @@ def update_chat_data(self, chat_id: int, data: Dict) -> None: data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. """ if self.chat_data is None: - self.chat_data = defaultdict(dict) + self.chat_data = cast( + MutableMapping[int, CD], + self.context_customizer.chat_data_mapping( # type: ignore[call-arg] + self.context_customizer.chat_data + ), + ) if self.chat_data.get(chat_id) == data: return self.chat_data[chat_id] = data @@ -282,7 +362,7 @@ def update_chat_data(self, chat_id: int, data: Dict) -> None: else: self.dump_singlefile() - def update_bot_data(self, data: Dict) -> None: + def update_bot_data(self, data: BD) -> None: """Will update the bot_data and depending on :attr:`on_flush` save the pickle file. Args: @@ -290,7 +370,7 @@ def update_bot_data(self, data: Dict) -> None: """ if self.bot_data == data: return - self.bot_data = data.copy() + self.bot_data = data if not self.on_flush: if not self.single_file: filename = f"{self.filename}_bot_data" diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 62186530f54..7547a6fa08e 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -252,27 +252,16 @@ def __init__( # type: ignore[no-untyped-def,misc] self.job_queue = JobQueue() self.__exception_event = Event() self.persistence = persistence - if context_customizer: - self.dispatcher = Dispatcher( - self.bot, - self.update_queue, - job_queue=self.job_queue, - workers=workers, - exception_event=self.__exception_event, - persistence=persistence, - use_context=use_context, - context_customizer=context_customizer, - ) - else: - self.dispatcher = Dispatcher( # type: ignore[assignment] - self.bot, - self.update_queue, - job_queue=self.job_queue, - workers=workers, - exception_event=self.__exception_event, - persistence=persistence, - use_context=use_context, - ) + self.dispatcher = Dispatcher( + self.bot, + self.update_queue, + job_queue=self.job_queue, + workers=workers, + exception_event=self.__exception_event, + persistence=persistence, + use_context=use_context, + context_customizer=context_customizer, + ) self.job_queue.set_dispatcher(self.dispatcher) else: con_pool_size = dispatcher.workers + 4 From 6eed0ea3fdac55c51247db5bc2b1b0d67079ff16 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Thu, 21 Jan 2021 20:21:25 +0100 Subject: [PATCH 09/31] Fix tests --- telegram/ext/handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index 10238cbadea..5764ebaad2b 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -211,9 +211,9 @@ def collect_optional_args( optional_args['job_queue'] = dispatcher.job_queue if self.pass_user_data and isinstance(update, Update): user = update.effective_user - optional_args['user_data'] = dispatcher.user_data[user.id] if user else None + optional_args['user_data'] = dispatcher.user_data[user.id if user else None] if self.pass_chat_data and isinstance(update, Update): chat = update.effective_chat - optional_args['chat_data'] = dispatcher.chat_data[chat.id] if chat else None + optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None] return optional_args From c93eb28eb464bb958dd7d78f33ec24b89886b238 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Thu, 21 Jan 2021 21:33:59 +0100 Subject: [PATCH 10/31] Add tests for PicklePersistence --- telegram/ext/picklepersistence.py | 4 +-- tests/test_persistence.py | 52 ++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index 4b6ec9c70a3..a66d03d1f1e 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -161,7 +161,7 @@ def load_singlefile(self) -> None: ) ) # For backwards compatibility with files not containing bot data - self.bot_data = data.get('bot_data', {}) + self.bot_data = data.get('bot_data', self.context_customizer.bot_data()) self.conversations = data['conversations'] except OSError: self.conversations = dict() @@ -264,7 +264,7 @@ def get_bot_data(self) -> BD: filename = f"{self.filename}_bot_data" data = self.load_file(filename) if not data: - data = {} + data = self.context_customizer.bot_data() self.bot_data = data else: self.load_singlefile() diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2d59216a2f9..48f88affd07 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -45,6 +45,7 @@ DictPersistence, TypeHandler, JobQueue, + ContextCustomizer, ) @@ -775,7 +776,11 @@ def update(bot): return Update(0, message=message) -class TestPickelPersistence: +class CustomMapping(defaultdict): + pass + + +class TestPicklePersistence: def test_no_files_present_multi_file(self, pickle_persistence): assert pickle_persistence.get_user_data() == defaultdict(dict) assert pickle_persistence.get_user_data() == defaultdict(dict) @@ -1364,6 +1369,51 @@ def job_callback(context): user_data = pickle_persistence.get_user_data() assert user_data[789] == {'test3': '123'} + @pytest.mark.parametrize('singlefile', [True, False]) + @pytest.mark.parametrize('udm', [defaultdict, CustomMapping]) + @pytest.mark.parametrize('cdm', [defaultdict, CustomMapping]) + @pytest.mark.parametrize('ud', [int, float, complex]) + @pytest.mark.parametrize('cd', [int, float, complex]) + @pytest.mark.parametrize('bd', [int, float, complex]) + def test_with_context_customizer(self, ud, cd, bd, udm, cdm, singlefile): + cc = ContextCustomizer( + user_data=ud, chat_data=cd, bot_data=bd, user_data_mapping=udm, chat_data_mapping=cdm + ) + persistence = PicklePersistence( + 'pickletest', single_file=singlefile, context_customizer=cc + ) + + assert isinstance(persistence.get_user_data(), udm) + assert isinstance(persistence.get_user_data()[1], ud) + assert persistence.get_user_data()[1] == 0 + assert isinstance(persistence.get_chat_data(), cdm) + assert isinstance(persistence.get_chat_data()[1], cd) + assert persistence.get_chat_data()[1] == 0 + assert isinstance(persistence.get_bot_data(), bd) + assert persistence.get_bot_data() == 0 + + persistence.user_data = None + persistence.chat_data = None + persistence.update_user_data(1, ud(1)) + persistence.update_chat_data(1, cd(1)) + persistence.update_bot_data(bd(1)) + assert persistence.get_user_data()[1] == 1 + assert persistence.get_chat_data()[1] == 1 + assert persistence.get_bot_data() == 1 + + persistence.flush() + persistence = PicklePersistence( + 'pickletest', single_file=singlefile, context_customizer=cc + ) + assert isinstance(persistence.get_user_data(), udm) + assert isinstance(persistence.get_user_data()[1], ud) + assert persistence.get_user_data()[1] == 1 + assert isinstance(persistence.get_chat_data(), cdm) + assert isinstance(persistence.get_chat_data()[1], cd) + assert persistence.get_chat_data()[1] == 1 + assert isinstance(persistence.get_bot_data(), bd) + assert persistence.get_bot_data() == 1 + @pytest.fixture(scope='function') def user_data_json(user_data): From c72559fabc42ddf8ac23ff69ee4d583888e3f70e Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Thu, 21 Jan 2021 21:49:21 +0100 Subject: [PATCH 11/31] Increase coverage --- setup.cfg | 1 + telegram/ext/updater.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1aebfe10b12..4d5aff02dbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ omit = [coverage:report] exclude_lines = if TYPE_CHECKING: + ... [mypy] warn_unused_ignores = True diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 7547a6fa08e..f93ab66b44c 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -208,12 +208,12 @@ def __init__( # type: ignore[no-untyped-def,misc] raise ValueError('`dispatcher` and `bot` are mutually exclusive') if persistence is not None: raise ValueError('`dispatcher` and `persistence` are mutually exclusive') - if workers is not None: - raise ValueError('`dispatcher` and `workers` are mutually exclusive') if use_context != dispatcher.use_context: raise ValueError('`dispatcher` and `use_context` are mutually exclusive') if context_customizer is not None: raise ValueError('`dispatcher` and `context_customizer` are mutually exclusive') + if workers is not None: + raise ValueError('`dispatcher` and `workers` are mutually exclusive') self.logger = logging.getLogger(__name__) From 1ff85031722e33878c3a666f6ad69a5b1a5788f4 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 30 Apr 2021 17:20:55 +0200 Subject: [PATCH 12/31] Start refactoring to introduce BasePersistence.refresh_*_data --- .../source/telegram.ext.contextcustomizer.rst | 9 - docs/source/telegram.ext.contexttypes.rst | 8 + docs/source/telegram.ext.rst | 5 +- docs/source/telegram.ext.utils.types.rst | 8 + telegram/ext/__init__.py | 50 +- telegram/ext/basepersistence.py | 67 +- telegram/ext/callbackcontext.py | 43 +- telegram/ext/callbackqueryhandler.py | 2 +- telegram/ext/chatmemberhandler.py | 2 +- telegram/ext/choseninlineresulthandler.py | 2 +- telegram/ext/commandhandler.py | 3 +- telegram/ext/contextcustomizer.py | 727 ------------------ telegram/ext/contexttypes.py | 202 +++++ telegram/ext/conversationhandler.py | 3 +- telegram/ext/dictpersistence.py | 24 +- telegram/ext/dispatcher.py | 48 +- telegram/ext/handler.py | 10 +- telegram/ext/inlinequeryhandler.py | 2 +- telegram/ext/messagehandler.py | 2 +- telegram/ext/picklepersistence.py | 142 ++-- telegram/ext/pollanswerhandler.py | 2 +- telegram/ext/pollhandler.py | 2 +- telegram/ext/precheckoutqueryhandler.py | 2 +- telegram/ext/regexhandler.py | 2 +- telegram/ext/shippingqueryhandler.py | 2 +- telegram/ext/stringcommandhandler.py | 2 +- telegram/ext/stringregexhandler.py | 2 +- telegram/ext/typehandler.py | 2 +- telegram/ext/updater.py | 25 +- telegram/ext/utils/types.py | 32 + telegram/utils/types.py | 21 - tests/test_dispatcher.py | 8 +- tests/test_jobqueue.py | 4 +- tests/test_persistence.py | 4 +- 34 files changed, 514 insertions(+), 955 deletions(-) delete mode 100644 docs/source/telegram.ext.contextcustomizer.rst create mode 100644 docs/source/telegram.ext.contexttypes.rst create mode 100644 docs/source/telegram.ext.utils.types.rst delete mode 100644 telegram/ext/contextcustomizer.py create mode 100644 telegram/ext/contexttypes.py create mode 100644 telegram/ext/utils/types.py diff --git a/docs/source/telegram.ext.contextcustomizer.rst b/docs/source/telegram.ext.contextcustomizer.rst deleted file mode 100644 index c08954f2d05..00000000000 --- a/docs/source/telegram.ext.contextcustomizer.rst +++ /dev/null @@ -1,9 +0,0 @@ -telegram.ext.ContextCustomizer -============================== - -.. - We manually set the signature here in order to not display all the overload variants - -.. autoclass:: telegram.ext.ContextCustomizer(context: Type[CCT], bot_data: Type[BD], chat_data: Type[CD], user_data: Type[UD], chat_data_mapping: Type[UDM], user_data_mapping: Type[CDM]) - :members: - :show-inheritance: diff --git a/docs/source/telegram.ext.contexttypes.rst b/docs/source/telegram.ext.contexttypes.rst new file mode 100644 index 00000000000..d0cc0a29a1d --- /dev/null +++ b/docs/source/telegram.ext.contexttypes.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/contexttypes.py + +telegram.ext.ContextTypes +========================= + +.. autoclass:: telegram.ext.ContextTypes + :members: + :show-inheritance: diff --git a/docs/source/telegram.ext.rst b/docs/source/telegram.ext.rst index ea7dede769c..a53c182fd70 100644 --- a/docs/source/telegram.ext.rst +++ b/docs/source/telegram.ext.rst @@ -13,7 +13,7 @@ telegram.ext package telegram.ext.messagequeue telegram.ext.delayqueue telegram.ext.callbackcontext - telegram.ext.contextcustomizer + telegram.ext.contexttypes telegram.ext.defaults Handlers @@ -54,4 +54,5 @@ utils .. toctree:: - telegram.ext.utils.promise \ No newline at end of file + telegram.ext.utils.promise + telegram.ext.utils.types \ No newline at end of file diff --git a/docs/source/telegram.ext.utils.types.rst b/docs/source/telegram.ext.utils.types.rst new file mode 100644 index 00000000000..5c501ecf840 --- /dev/null +++ b/docs/source/telegram.ext.utils.types.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/utils/types.py + +telegram.ext.utils.types Module +================================ + +.. automodule:: telegram.ext.utils.types + :members: + :show-inheritance: diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 9795abf0204..6854f7114c3 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -23,7 +23,7 @@ from .dictpersistence import DictPersistence from .handler import Handler from .callbackcontext import CallbackContext -from .contextcustomizer import ContextCustomizer +from .contexttypes import ContextTypes from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async from .jobqueue import JobQueue, Job from .updater import Updater @@ -48,39 +48,39 @@ from .defaults import Defaults __all__ = ( - 'Dispatcher', - 'JobQueue', - 'Job', - 'Updater', + 'BaseFilter', + 'BasePersistence', + 'CallbackContext', 'CallbackQueryHandler', + 'ChatMemberHandler', 'ChosenInlineResultHandler', 'CommandHandler', + 'ContextTypes', + 'ConversationHandler', + 'Defaults', + 'DelayQueue', + 'DictPersistence', + 'Dispatcher', + 'DispatcherHandlerStop', + 'Filters', 'Handler', 'InlineQueryHandler', - 'MessageHandler', - 'BaseFilter', + 'Job', + 'JobQueue', 'MessageFilter', - 'UpdateFilter', - 'Filters', + 'MessageHandler', + 'MessageQueue', + 'PicklePersistence', + 'PollAnswerHandler', + 'PollHandler', + 'PreCheckoutQueryHandler', + 'PrefixHandler', 'RegexHandler', + 'ShippingQueryHandler', 'StringCommandHandler', 'StringRegexHandler', 'TypeHandler', - 'ConversationHandler', - 'PreCheckoutQueryHandler', - 'ShippingQueryHandler', - 'MessageQueue', - 'DelayQueue', - 'DispatcherHandlerStop', + 'UpdateFilter', + 'Updater', 'run_async', - 'CallbackContext', - 'BasePersistence', - 'PicklePersistence', - 'DictPersistence', - 'PrefixHandler', - 'PollAnswerHandler', - 'PollHandler', - 'ChatMemberHandler', - 'Defaults', - 'ContextCustomizer', ) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index d00630e1281..436d5125ae7 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -20,14 +20,15 @@ import warnings from abc import ABC, abstractmethod from copy import copy -from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, Mapping +from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, Mapping, DefaultDict from telegram import Bot -from telegram.utils.types import ConversationDict, CD, UD, BD, UDM, CDM +from telegram.utils.types import ConversationDict +from telegram.ext.utils.types import UD, CD, BD -class BasePersistence(Generic[UD, CD, BD, UDM, CDM], ABC): +class BasePersistence(Generic[UD, CD, BD], ABC): """Interface class for adding persistence to your bot. Subclass this object for different implementations of a persistent bot. @@ -35,10 +36,13 @@ class BasePersistence(Generic[UD, CD, BD, UDM, CDM], ABC): * :meth:`get_bot_data` * :meth:`update_bot_data` + * :meth:`refresh_bot_data` * :meth:`get_chat_data` * :meth:`update_chat_data` + * :meth:`refresh_chat_data` * :meth:`get_user_data` * :meth:`update_user_data` + * :meth:`refresh_user_data` * :meth:`get_conversations` * :meth:`update_conversation` * :meth:`flush` @@ -290,33 +294,33 @@ def _insert_bot(self, obj: object, memo: Dict[int, object]) -> object: # pylint return obj @abstractmethod - def get_user_data(self) -> Mapping[int, UD]: + def get_user_data(self) -> DefaultDict[int, UD]: """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``user_data`` if stored, or an empty - ``Mapping`` with integer keys. + :obj:`defaultdict(telegram.ext.utils.types.UD)` with integer keys. Returns: - :obj:`Mapping[int, UD]`: The restored user data. + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data. """ @abstractmethod - def get_chat_data(self) -> Mapping[int, CD]: + def get_chat_data(self) -> DefaultDict[int, CD]: """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``chat_data`` if stored, or an empty - ``Mapping`` with integer keys. + :obj:`defaultdict(telegram.ext.utils.types.CD)` with integer keys. Returns: - :obj:`Mapping[int, Any]`: The restored chat data. + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data. """ @abstractmethod def get_bot_data(self) -> BD: """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``bot_data`` if stored, or an empty - :obj:`dict`. + :class:`telegram.ext.utils.types.BD`. Returns: - :obj:`dict`: The restored bot data. + :class:`telegram.ext.utils.types.BD`: The restored user data. """ @abstractmethod @@ -353,7 +357,8 @@ def update_user_data(self, user_id: int, data: UD) -> None: Args: user_id (:obj:`int`): The user the data might have been changed for. - data (:obj:`UD`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. + data (:class:`telegram.ext.utils.types.UD`): The + :attr:`telegram.ext.dispatcher.user_data` ``[user_id]``. """ @abstractmethod @@ -363,7 +368,8 @@ def update_chat_data(self, chat_id: int, data: CD) -> None: Args: chat_id (:obj:`int`): The chat the data might have been changed for. - data (:obj:`CD`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. + data (:class:`telegram.ext.utils.types.CD`): The + :attr:`telegram.ext.dispatcher.chat_data` ``[chat_id]``. """ @abstractmethod @@ -372,9 +378,42 @@ def update_bot_data(self, data: BD) -> None: handled an update. Args: - data (:obj:`BD`): The :attr:`telegram.ext.dispatcher.bot_data` . + data (:class:`telegram.ext.utils.types.BD`): The + :attr:`telegram.ext.dispatcher.bot_data`. """ + def refresh_user_data(self, user_id: int, user_data: UD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` before passing the + :attr:`user_data` to a callback. Can be used to update data stored in :attr:`user_data` + from an external source. + + Args: + user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with. + user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user. + """ + raise NotImplementedError + + def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` before passing the + :attr:`chat_data` to a callback. Can be used to update data stored in :attr:`chat_data` + from an external source. + + Args: + chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with. + chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat. + """ + raise NotImplementedError + + def refresh_bot_data(self, bot_data: BD) -> None: + """Will be called by the :class:`telegram.ext.Dispatcher` before passing the + :attr:`bot_data` to a callback. Can be used to update data stored in :attr:`bot_data` + from an external source. + + Args: + bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``. + """ + raise NotImplementedError + def flush(self) -> None: """Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the persistence a chance to finish up saving or close a database connection gracefully. diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index 1b3f90f1087..a0cb08946c5 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Dict, List, Match, NoReturn, Optional, Tuple, Union, Generic from telegram import Update -from telegram.utils.types import UD, CD, BD +from telegram.ext.utils.types import UD, CD, BD if TYPE_CHECKING: from telegram import Bot @@ -87,9 +87,8 @@ def __init__(self, dispatcher: 'Dispatcher'): 'CallbackContext should not be used with a non context aware ' 'dispatcher!' ) self._dispatcher = dispatcher - self._bot_data = dispatcher.bot_data - self._chat_data: Optional[CD] = None - self._user_data: Optional[UD] = None + self._chat_id_and_data: Optional[Tuple[int, CD]] = None + self._user_id_and_data: Optional[Tuple[int, UD]] = None self.args: Optional[List[str]] = None self.matches: Optional[List[Match]] = None self.error: Optional[Exception] = None @@ -108,7 +107,7 @@ def bot_data(self) -> BD: bot_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each update it will be the same :obj:`dict`. """ - return self._bot_data + return self.dispatcher.bot_data @bot_data.setter def bot_data(self, value: object) -> NoReturn: @@ -129,7 +128,9 @@ def chat_data(self) -> Optional[CD]: `_. """ - return self._chat_data + if self._chat_id_and_data: + return self._chat_id_and_data[1] + return None @chat_data.setter def chat_data(self, value: object) -> NoReturn: @@ -143,7 +144,9 @@ def user_data(self) -> Optional[UD]: user_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each update from the same user it will be the same :obj:`dict`. """ - return self._user_data + if self._user_id_and_data: + return self._user_id_and_data[1] + return None @user_data.setter def user_data(self, value: object) -> NoReturn: @@ -151,6 +154,22 @@ def user_data(self, value: object) -> NoReturn: "You can not assign a new value to user_data, see https://git.io/Jt6ic" ) + def refresh_data(self) -> None: + """ + If :attr:`dispatcher` uses persistence, calls + :meth:`telegram.ext.BasePersistence.refresh_bot_data` on :attr:`bot_data`, + :meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data`, + :meth:`telegram.ext.BasePersistence.refresh_user_data` on :attr:`user_data`, if + appropriate. + """ + if self.dispatcher.persistence: + if self.dispatcher.persistence.store_bot_data: + self.dispatcher.persistence.refresh_bot_data(self.bot_data) + if self.dispatcher.persistence.store_chat_data and self._chat_id_and_data is not None: + self.dispatcher.persistence.refresh_chat_data(*self._chat_id_and_data) + if self.dispatcher.persistence.store_user_data and self._user_id_and_data is not None: + self.dispatcher.persistence.refresh_user_data(*self._user_id_and_data) + @classmethod def from_error( cls, @@ -213,9 +232,15 @@ def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackConte user = update.effective_user if chat: - self._chat_data = dispatcher.chat_data[chat.id] # pylint: disable=W0212 + self._chat_id_and_data = ( + chat.id, + dispatcher.chat_data[chat.id], # pylint: disable=W0212 + ) if user: - self._user_data = dispatcher.user_data[user.id] # pylint: disable=W0212 + self._user_id_and_data = ( + user.id, + dispatcher.user_data[user.id], # pylint: disable=W0212 + ) return self @classmethod diff --git a/telegram/ext/callbackqueryhandler.py b/telegram/ext/callbackqueryhandler.py index a43d6f6d5c3..9b814a0bdc6 100644 --- a/telegram/ext/callbackqueryhandler.py +++ b/telegram/ext/callbackqueryhandler.py @@ -35,7 +35,7 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher diff --git a/telegram/ext/chatmemberhandler.py b/telegram/ext/chatmemberhandler.py index bcacc2f3c81..49cef232811 100644 --- a/telegram/ext/chatmemberhandler.py +++ b/telegram/ext/chatmemberhandler.py @@ -22,7 +22,7 @@ from telegram import Update from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT RT = TypeVar('RT') diff --git a/telegram/ext/choseninlineresulthandler.py b/telegram/ext/choseninlineresulthandler.py index 4b72214120e..9217b7df3b7 100644 --- a/telegram/ext/choseninlineresulthandler.py +++ b/telegram/ext/choseninlineresulthandler.py @@ -23,7 +23,7 @@ from telegram import Update from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT RT = TypeVar('RT') diff --git a/telegram/ext/commandhandler.py b/telegram/ext/commandhandler.py index abee45e5e58..f2acd195e24 100644 --- a/telegram/ext/commandhandler.py +++ b/telegram/ext/commandhandler.py @@ -24,9 +24,10 @@ from telegram import MessageEntity, Update from telegram.ext import BaseFilter, Filters from telegram.utils.deprecate import TelegramDeprecationWarning -from telegram.utils.types import SLT, CCT +from telegram.utils.types import SLT from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from .utils.types import CCT from .handler import Handler if TYPE_CHECKING: diff --git a/telegram/ext/contextcustomizer.py b/telegram/ext/contextcustomizer.py deleted file mode 100644 index 880a75e2f89..00000000000 --- a/telegram/ext/contextcustomizer.py +++ /dev/null @@ -1,727 +0,0 @@ -#!/usr/bin/env python -# -# A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2020 -# Leandro Toledo de Souza -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser Public License for more details. -# -# You should have received a copy of the GNU Lesser Public License -# along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=R0201 -"""This module contains the auxiliary class ContextCustomizer.""" -from collections import defaultdict -from collections.abc import Mapping -from typing import Type, Any, NoReturn, Generic, overload, Dict # pylint: disable=W0611 - -from telegram.ext.callbackcontext import CallbackContext -from telegram.utils.types import CCT, UD, CD, BD, CDM, UDM, IntDD # pylint: disable=W0611 - - -class ContextCustomizer(Generic[CCT, UD, CD, BD, UDM, CDM]): - """ - Convenience class to gather customizable types of the ``context`` interface. - - Args: - context (:obj:`type`, optional): Determines the type of the ``context`` argument of all - (error-)handler callbacks and job callbacks. Must be a subclass of - :class:`telegram.ext.CallbackContext`. Defaults to - :class:`telegram.ext.CallbackContext`. - bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data`` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support - instantiating without arguments. - chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. - user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. - chat_data_mapping (:obj:`type`, optional): In combination with :attr:`chat_data` determines - the type of :attr:`telegram.ext.Dispatcher.chat_data`. Must be a subclass of - :obj:`collections.abc.Mapping` with integer keys supporting instantiating via - - .. code:: python - - chat_data_mapping(chat_data) - - Defaults to :obj:`defaultdict`. - - user_data_mapping (:obj:`type`, optional): In combination with :attr:`user_data` determines - the type of :attr:`telegram.ext.Dispatcher.user_data`. Must be a subclass of - :obj:`collections.abc.Mapping` with integer keys supporting instantiating via - - .. code:: python - - user_data_mapping(user_data) - - Defaults to :obj:`defaultdict`. - - """ - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, " - "IntDD[Dict], IntDD[Dict]]", - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, Dict, IntDD[Dict], IntDD[Dict]]", - context: Type[CCT], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, " - "IntDD[UD], IntDD[Dict]]", - bot_data: Type[BD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, " - "IntDD[Dict], IntDD[CD]]", - chat_data: Type[CD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, " - "IntDD[Dict], IntDD[Dict]]", - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, " - "Dict, UDM, IntDD[Dict]]", - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, " - "IntDD[Dict], CDM]", - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, Dict, IntDD[UD], IntDD[Dict]]", - context: Type[CCT], - bot_data: Type[BD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, Dict, IntDD[Dict], IntDD[CD]]", - context: Type[CCT], - chat_data: Type[CD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, BD, IntDD[Dict], IntDD[Dict]]", - context: Type[CCT], - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, Dict, UDM, IntDD[Dict]]", - context: Type[CCT], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, Dict, IntDD[Dict], CDM]", - context: Type[CCT], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, " - "IntDD[UD], IntDD[CD]]", - bot_data: Type[BD], - chat_data: Type[CD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, " - "IntDD[UD], IntDD[Dict]]", - bot_data: Type[BD], - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, " - "UDM, IntDD[Dict]]", - bot_data: Type[BD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, " - "IntDD[UD], CDM]", - bot_data: Type[BD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, " - "IntDD[Dict], IntDD[CD]]", - chat_data: Type[CD], - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, " - "UDM, IntDD[CD]]", - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, " - "IntDD[Dict], CDM]", - chat_data: Type[CD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, UDM, " - "IntDD[Dict]]", - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, " - "IntDD[Dict], CDM]", - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, UDM, CDM]", - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, Dict, IntDD[UD], IntDD[CD]]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, BD, IntDD[UD], IntDD[Dict]]", - context: Type[CCT], - bot_data: Type[BD], - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, Dict, UDM, IntDD[Dict]]", - context: Type[CCT], - bot_data: Type[BD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, Dict, IntDD[UD], CDM]", - context: Type[CCT], - bot_data: Type[BD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, BD, IntDD[Dict], IntDD[CD]]", - context: Type[CCT], - chat_data: Type[CD], - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, Dict, UDM, IntDD[CD]]", - context: Type[CCT], - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, Dict, IntDD[Dict], CDM]", - context: Type[CCT], - chat_data: Type[CD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, BD, UDM, IntDD[Dict]]", - context: Type[CCT], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, BD, IntDD[Dict], CDM]", - context: Type[CCT], - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, Dict, UDM, CDM]", - context: Type[CCT], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, IntDD[UD], IntDD[CD]]", - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, UDM, IntDD[CD]]", - bot_data: Type[BD], - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, IntDD[UD], CDM]", - bot_data: Type[BD], - chat_data: Type[CD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, UDM, IntDD[Dict]]", - bot_data: Type[BD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, IntDD[UD], CDM]", - bot_data: Type[BD], - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, Dict], UD, Dict, Dict, UDM, CDM]", - bot_data: Type[BD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, UDM, IntDD[CD]]", - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, IntDD[Dict], CDM]", - chat_data: Type[CD], - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, Dict], Dict, CD, Dict, UDM, CDM]", - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, Dict, BD], Dict, Dict, BD, UDM, CDM]", - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, BD, IntDD[UD], IntDD[CD]]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, Dict, UDM, IntDD[CD]]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, Dict, IntDD[UD], CDM]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, BD, UDM, IntDD[Dict]]", - context: Type[CCT], - bot_data: Type[BD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, BD, IntDD[UD], CDM]", - context: Type[CCT], - bot_data: Type[BD], - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, Dict, UDM, CDM]", - context: Type[CCT], - bot_data: Type[BD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, BD, UDM, IntDD[CD]]", - context: Type[CCT], - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, BD, IntDD[Dict], CDM]", - context: Type[CCT], - chat_data: Type[CD], - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, Dict, UDM, CDM]", - context: Type[CCT], - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, Dict, BD, UDM, CDM]", - context: Type[CCT], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, UDM, IntDD[CD]]", - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, IntDD[UD], CDM]", - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, Dict], UD, CD, Dict, UDM, CDM]", - bot_data: Type[BD], - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, Dict, BD], UD, Dict, BD, UDM, CDM]", - bot_data: Type[BD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[Dict, CD, BD], Dict, CD, BD, UDM, CDM]", - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, BD, UDM, IntDD[CD]]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, BD, IntDD[UD], CDM]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, Dict, UDM, CDM]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, Dict, BD, UDM, CDM]", - context: Type[CCT], - bot_data: Type[BD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, Dict, CD, BD, UDM, CDM]", - context: Type[CCT], - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CallbackContext[UD, CD, BD], UD, CD, BD, UDM, CDM]", - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - @overload - def __init__( - self: "ContextCustomizer[CCT, UD, CD, BD, UDM, CDM]", - context: Type[CCT], - bot_data: Type[BD], - chat_data: Type[CD], - user_data: Type[UD], - chat_data_mapping: Type[UDM], - user_data_mapping: Type[CDM], - ): - ... - - def __init__( # type: ignore[no-untyped-def] - self, - context=CallbackContext, - bot_data=dict, - chat_data=dict, - user_data=dict, - chat_data_mapping=defaultdict, - user_data_mapping=defaultdict, - ): - if not issubclass(context, CallbackContext): - raise ValueError('context must be a subclass of CallbackContext.') - if not issubclass(chat_data_mapping, Mapping): - raise ValueError('chat_data_mapping must be a subclass of collections.Mapping.') - if not issubclass(user_data_mapping, Mapping): - raise ValueError('user_data_mapping must be a subclass of collections.Mapping.') - - self._context = context - self._bot_data = bot_data - self._chat_data = chat_data - self._user_data = user_data - self._chat_data_mapping = chat_data_mapping - self._user_data_mapping = user_data_mapping - - @property - def context(self) -> Type[CCT]: - return self._context - - @context.setter - def context(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") - - @property - def bot_data(self) -> Type[BD]: - return self._bot_data - - @bot_data.setter - def bot_data(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") - - @property - def chat_data(self) -> Type[CD]: - return self._chat_data - - @chat_data.setter - def chat_data(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") - - @property - def user_data(self) -> Type[UD]: - return self._user_data - - @user_data.setter - def user_data(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") - - @property - def user_data_mapping(self) -> Type[UDM]: - return self._user_data_mapping - - @user_data_mapping.setter - def user_data_mapping(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") - - @property - def chat_data_mapping(self) -> Type[CDM]: - return self._chat_data_mapping - - @chat_data_mapping.setter - def chat_data_mapping(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextCustomizer attributes.") diff --git a/telegram/ext/contexttypes.py b/telegram/ext/contexttypes.py new file mode 100644 index 00000000000..10286e40c2e --- /dev/null +++ b/telegram/ext/contexttypes.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2020 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=R0201 +"""This module contains the auxiliary class ContextTypes.""" +from typing import Type, Any, NoReturn, Generic, overload, Dict # pylint: disable=W0611 + +from telegram.ext.callbackcontext import CallbackContext +from telegram.ext.utils.types import CCT, UD, CD, BD + + +class ContextTypes(Generic[CCT, UD, CD, BD]): + """ + Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext` + interface. + + Args: + context (:obj:`type`, optional): Determines the type of the ``context`` argument of all + (error-)handler callbacks and job callbacks. Must be a subclass of + :class:`telegram.ext.CallbackContext`. Defaults to + :class:`telegram.ext.CallbackContext`. + bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments. + chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + + """ + + @overload + def __init__( + self: 'ContextTypes[CallbackContext, Dict, Dict, Dict]', + ): + ... + + @overload + def __init__(self: 'ContextTypes[CCT, Dict, Dict, Dict]', context: Type[CCT]): + ... + + @overload + def __init__(self: 'ContextTypes[CallbackContext, UD, Dict, Dict]', bot_data: Type[UD]): + ... + + @overload + def __init__(self: 'ContextTypes[CallbackContext, Dict, CD, Dict]', chat_data: Type[CD]): + ... + + @overload + def __init__(self: 'ContextTypes[CallbackContext, Dict, Dict, BD]', user_data: Type[BD]): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, Dict, Dict]', context: Type[CCT], bot_data: Type[UD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, Dict, CD, Dict]', context: Type[CCT], chat_data: Type[CD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, Dict, Dict, BD]', context: Type[CCT], user_data: Type[BD] + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext, UD, CD, Dict]', + bot_data: Type[UD], + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext, UD, Dict, BD]', + bot_data: Type[UD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext, Dict, CD, BD]', + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, CD, Dict]', + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, Dict, BD]', + context: Type[CCT], + bot_data: Type[UD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, Dict, CD, BD]', + context: Type[CCT], + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CallbackContext, UD, CD, BD]', + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + @overload + def __init__( + self: 'ContextTypes[CCT, UD, CD, BD]', + context: Type[CCT], + bot_data: Type[UD], + chat_data: Type[CD], + user_data: Type[BD], + ): + ... + + def __init__( # type: ignore[no-untyped-def] + self, + context=CallbackContext, + bot_data=dict, + chat_data=dict, + user_data=dict, + ): + if not issubclass(context, CallbackContext): + raise ValueError('context must be a subclass of CallbackContext.') + + self._context = context + self._bot_data = bot_data + self._chat_data = chat_data + self._user_data = user_data + + @property + def context(self) -> Type[CCT]: + return self._context + + @context.setter + def context(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextTypes attributes.") + + @property + def bot_data(self) -> Type[BD]: + return self._bot_data + + @bot_data.setter + def bot_data(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextTypes attributes.") + + @property + def chat_data(self) -> Type[CD]: + return self._chat_data + + @chat_data.setter + def chat_data(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextTypes attributes.") + + @property + def user_data(self) -> Type[UD]: + return self._user_data + + @user_data.setter + def user_data(self, value: Any) -> NoReturn: + raise AttributeError("You can not assign a new value to ContextTypes attributes.") diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py index 157e79f6d6b..e36c32aab18 100644 --- a/telegram/ext/conversationhandler.py +++ b/telegram/ext/conversationhandler.py @@ -37,7 +37,8 @@ InlineQueryHandler, ) from telegram.ext.utils.promise import Promise -from telegram.utils.types import ConversationDict, CCT +from telegram.utils.types import ConversationDict +from telegram.ext.utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher, Job diff --git a/telegram/ext/dictpersistence.py b/telegram/ext/dictpersistence.py index 572a27e85a7..7224356a881 100644 --- a/telegram/ext/dictpersistence.py +++ b/telegram/ext/dictpersistence.py @@ -37,7 +37,7 @@ class DictPersistence(BasePersistence): - """Using python's dicts and json for making your bot persistent. + """Using Python's :obj:`dict` and ``json`` for making your bot persistent. Note: This class does *not* implement a :meth:`flush` method, meaning that data managed by @@ -253,7 +253,7 @@ def update_user_data(self, user_id: int, data: Dict) -> None: Args: user_id (:obj:`int`): The user the data might have been changed for. - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. + data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` ``[user_id]``. """ if self._user_data is None: self._user_data = defaultdict(dict) @@ -267,7 +267,7 @@ def update_chat_data(self, chat_id: int, data: Dict) -> None: Args: chat_id (:obj:`int`): The chat the data might have been changed for. - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. + data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` ``[chat_id]``. """ if self._chat_data is None: self._chat_data = defaultdict(dict) @@ -286,3 +286,21 @@ def update_bot_data(self, data: Dict) -> None: return self._bot_data = data.copy() self._bot_data_json = None + + def refresh_user_data(self, user_id: int, user_data: Dict) -> None: + """Does nothing. + + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` + """ + + def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None: + """Does nothing. + + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` + """ + + def refresh_bot_data(self, bot_data: Dict) -> None: + """Does nothing. + + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` + """ diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 1a1ac90912c..faf060908fd 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -21,6 +21,7 @@ import logging import warnings import weakref +from collections import defaultdict from functools import wraps from queue import Empty, Queue from threading import BoundedSemaphore, Event, Lock, Thread, current_thread @@ -37,17 +38,18 @@ TypeVar, overload, cast, + DefaultDict, ) from uuid import uuid4 from telegram import TelegramError, Update -from telegram.ext import BasePersistence, ContextCustomizer +from telegram.ext import BasePersistence, ContextTypes from telegram.ext.callbackcontext import CallbackContext from telegram.ext.handler import Handler from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.ext.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE -from telegram.utils.types import CCT, UD, CD, BD, UDM, CDM, IntDD # pylint: disable=W0611 +from telegram.ext.utils.types import CCT, UD, CD, BD if TYPE_CHECKING: from telegram import Bot @@ -117,7 +119,7 @@ def __init__(self, state: object = None) -> None: self.state = state -class Dispatcher(Generic[CCT, UD, CD, BD, UDM, CDM]): +class Dispatcher(Generic[CCT, UD, CD, BD]): """This class dispatches all kinds of updates to its registered handlers. Args: @@ -149,7 +151,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD, UDM, CDM]): bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot. persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to store data that should be persistent over restarts. - context_customizer (:class:`telegram.ext.ContextCustomizer`): Container for the types used + context_customizer (:class:`telegram.ext.ContextTypes`): Container for the types used in the ``context`` interface. """ @@ -161,8 +163,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD, UDM, CDM]): @overload def __init__( - self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict, ' - 'IntDD[Dict], IntDD[Dict]]', + self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]', bot: 'Bot', update_queue: Queue, workers: int = 4, @@ -175,7 +176,7 @@ def __init__( @overload def __init__( - self: 'Dispatcher[CCT, UD, CD, BD, UDM, CDM]', + self: 'Dispatcher[CCT, UD, CD, BD]', bot: 'Bot', update_queue: Queue, workers: int = 4, @@ -183,7 +184,7 @@ def __init__( job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, - context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, + context_customizer: ContextTypes[CCT, UD, CD, BD] = None, ): ... @@ -196,7 +197,7 @@ def __init__( job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, - context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, + context_customizer: ContextTypes[CCT, UD, CD, BD] = None, ): self.bot = bot self.update_queue = update_queue @@ -204,7 +205,7 @@ def __init__( self.workers = workers self.use_context = use_context self.context_customizer = cast( - ContextCustomizer[CCT, UD, CD, BD, UDM, CDM], context_customizer or ContextCustomizer() + ContextTypes[CCT, UD, CD, BD], context_customizer or ContextTypes() ) if not use_context: @@ -219,12 +220,8 @@ def __init__( 'Asynchronous callbacks can not be processed without at least one worker thread.' ) - self.user_data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] - self.context_customizer.user_data - ) - self.chat_data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] - self.context_customizer.chat_data - ) + self.user_data: DefaultDict[int, UD] = defaultdict(self.context_customizer.user_data) + self.chat_data: DefaultDict[int, CD] = defaultdict(self.context_customizer.chat_data) self.bot_data = self.context_customizer.bot_data() self.persistence: Optional[BasePersistence] = None self._update_persistence_lock = Lock() @@ -234,19 +231,13 @@ def __init__( self.persistence = persistence self.persistence.set_bot(self.bot) if self.persistence.store_user_data: - self.user_data = self.persistence.get_user_data() # type: ignore[assignment] - if not isinstance(self.user_data, self.context_customizer.user_data_mapping): - raise ValueError( - f"user_data must be of type " - f"{self.context_customizer.user_data_mapping.__name__}" - ) + self.user_data = self.persistence.get_user_data() + if not isinstance(self.user_data, defaultdict): + raise ValueError("user_data must be of type defaultdict") if self.persistence.store_chat_data: - self.chat_data = self.persistence.get_chat_data() # type: ignore[assignment] - if not isinstance(self.chat_data, self.context_customizer.chat_data_mapping): - raise ValueError( - f"chat_data must be of type " - f"{self.context_customizer.chat_data_mapping.__name__}" - ) + self.chat_data = self.persistence.get_chat_data() + if not isinstance(self.chat_data, defaultdict): + raise ValueError("chat_data must be of type defaultdict") if self.persistence.store_bot_data: self.bot_data = self.persistence.get_bot_data() if not isinstance(self.bot_data, self.context_customizer.bot_data): @@ -507,6 +498,7 @@ def process_update(self, update: object) -> None: if check is not None and check is not False: if not context and self.use_context: context = self.context_customizer.context.from_update(update, self) + context.refresh_data() handled = True sync_modes.append(handler.run_async) handler.handle_update(update, self, check, context) diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index e8f39441502..6b36e081d98 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -24,7 +24,7 @@ from telegram import Update from telegram.ext.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE -from telegram.utils.types import CCT +from telegram.ext.utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher @@ -211,9 +211,13 @@ def collect_optional_args( optional_args['job_queue'] = dispatcher.job_queue if self.pass_user_data and isinstance(update, Update): user = update.effective_user - optional_args['user_data'] = dispatcher.user_data[user.id if user else None] + optional_args['user_data'] = dispatcher.user_data[ + user.id if user else None # type: ignore[index] + ] if self.pass_chat_data and isinstance(update, Update): chat = update.effective_chat - optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None] + optional_args['chat_data'] = dispatcher.chat_data[ + chat.id if chat else None # type: ignore[index] + ] return optional_args diff --git a/telegram/ext/inlinequeryhandler.py b/telegram/ext/inlinequeryhandler.py index 6124430aaa9..b9d9fc5cdf2 100644 --- a/telegram/ext/inlinequeryhandler.py +++ b/telegram/ext/inlinequeryhandler.py @@ -35,7 +35,7 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher diff --git a/telegram/ext/messagehandler.py b/telegram/ext/messagehandler.py index 0524dcb07c2..39be657d01b 100644 --- a/telegram/ext/messagehandler.py +++ b/telegram/ext/messagehandler.py @@ -27,7 +27,7 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index ac55654f97c..3586c386210 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -18,17 +18,24 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the PicklePersistence class.""" import pickle -from typing import Any, Dict, Optional, Tuple, overload, Mapping, cast, TypeVar, MutableMapping +from collections import defaultdict +from typing import ( + Any, + Dict, + Optional, + Tuple, + overload, + cast, + DefaultDict, +) from telegram.ext import BasePersistence -from telegram.utils.types import ConversationDict, CD, UD, BD, IntDD # pylint: disable=W0611 -from .contextcustomizer import ContextCustomizer +from telegram.utils.types import ConversationDict # pylint: disable=W0611 +from .utils.types import UD, CD, BD +from .contexttypes import ContextTypes -UDM = TypeVar('UDM', bound=MutableMapping) -CDM = TypeVar('CDM', bound=MutableMapping) - -class PicklePersistence(BasePersistence[UD, CD, BD, UDM, CDM]): +class PicklePersistence(BasePersistence[UD, CD, BD]): """Using python's builtin pickle for making you bot persistent. Warning: @@ -58,20 +65,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD, UDM, CDM]): Default is :obj:`False`. context_customizer (:class:`telegram.ext.ContextCustomizer`, optional): Pass an instance of :class:`telegram.ext.ContextCustomizer` to customize the the types used in the - ``context`` interface. - - Note: - The types for :attr:`telegram.ext.ContextCustomizer.user_data_mapping` and - :attr:`telegram.ext.ContextCustomizer.chat_data_mapping` must be subclasses of - :class:`collections.abc.MutableMapping` and support instantiation via - - .. code:: python - - chat/user_data_mapping(chat/user_data_type[, data]) - - where ``data`` is of type ``chat/user_data_mapping``. - - If not passed, the defaults documented in + ``context`` interface. If not passed, the defaults documented in :class:`telegram.ext.ContextCustomizer` will be used. Attributes: @@ -90,13 +84,13 @@ class PicklePersistence(BasePersistence[UD, CD, BD, UDM, CDM]): :meth:`flush` is called and keep data in memory until that happens. When :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. - context_customizer (:class:`telegram.ext.ContextCustomizer`): Container for the types used + context_customizer (:class:`telegram.ext.ContextTypes`): Container for the types used in the ``context`` interface. """ @overload def __init__( - self: 'PicklePersistence[Dict, Dict, Dict, IntDD[Dict], IntDD[Dict]]', + self: 'PicklePersistence[Dict, Dict, Dict]', filename: str, store_user_data: bool = True, store_chat_data: bool = True, @@ -108,14 +102,14 @@ def __init__( @overload def __init__( - self: 'PicklePersistence[UD, CD, BD, UDM, CDM]', + self: 'PicklePersistence[UD, CD, BD]', filename: str, store_user_data: bool = True, store_chat_data: bool = True, store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, - context_customizer: ContextCustomizer[Any, UD, CD, BD, UDM, CDM] = None, + context_customizer: ContextTypes[Any, UD, CD, BD] = None, ): ... @@ -127,7 +121,7 @@ def __init__( store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, - context_customizer: ContextCustomizer[Any, UD, CD, BD, UDM, CDM] = None, + context_customizer: ContextTypes[Any, UD, CD, BD] = None, ): super().__init__( store_user_data=store_user_data, @@ -137,12 +131,12 @@ def __init__( self.filename = filename self.single_file = single_file self.on_flush = on_flush - self.user_data: Optional[MutableMapping[int, UD]] = None - self.chat_data: Optional[MutableMapping[int, CD]] = None + self.user_data: Optional[DefaultDict[int, UD]] = None + self.chat_data: Optional[DefaultDict[int, CD]] = None self.bot_data: Optional[BD] = None self.conversations: Optional[Dict[str, Dict[Tuple, object]]] = None self.context_customizer = cast( - ContextCustomizer[Any, UD, CD, BD, UDM, CDM], context_customizer or ContextCustomizer() + ContextTypes[Any, UD, CD, BD], context_customizer or ContextTypes() ) def load_singlefile(self) -> None: @@ -150,27 +144,15 @@ def load_singlefile(self) -> None: filename = self.filename with open(self.filename, "rb") as file: data = pickle.load(file) - self.user_data = ( - self.context_customizer.user_data_mapping( # type: ignore[call-arg] - self.context_customizer.user_data, data['user_data'] - ) - ) - self.chat_data = ( - self.context_customizer.chat_data_mapping( # type: ignore[call-arg] - self.context_customizer.chat_data, data['chat_data'] - ) - ) + self.user_data = defaultdict(self.context_customizer.user_data, data['user_data']) + self.chat_data = defaultdict(self.context_customizer.chat_data, data['chat_data']) # For backwards compatibility with files not containing bot data self.bot_data = data.get('bot_data', self.context_customizer.bot_data()) self.conversations = data['conversations'] except OSError: self.conversations = {} - self.user_data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] - self.context_customizer.user_data - ) - self.chat_data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] - self.context_customizer.chat_data - ) + self.user_data = defaultdict(self.context_customizer.user_data) + self.chat_data = defaultdict(self.context_customizer.chat_data) self.bot_data = self.context_customizer.bot_data() except pickle.UnpicklingError as exc: raise TypeError(f"File {filename} does not contain valid pickle data") from exc @@ -204,11 +186,11 @@ def dump_file(filename: str, data: object) -> None: with open(filename, "wb") as file: pickle.dump(data, file) - def get_user_data(self) -> Mapping[int, UD]: + def get_user_data(self) -> DefaultDict[int, UD]: """Returns the user_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: - :obj:`defaultdict`: The restored user data. + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data. """ if self.user_data: pass @@ -216,23 +198,19 @@ def get_user_data(self) -> Mapping[int, UD]: filename = f"{self.filename}_user_data" data = self.load_file(filename) if not data: - data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] - self.context_customizer.user_data - ) + data = defaultdict(self.context_customizer.user_data) else: - data = self.context_customizer.user_data_mapping( # type: ignore[call-arg] - self.context_customizer.user_data, data - ) + data = defaultdict(self.context_customizer.user_data, data) self.user_data = data else: self.load_singlefile() return self.user_data # type: ignore[return-value] - def get_chat_data(self) -> Mapping[int, CD]: + def get_chat_data(self) -> DefaultDict[int, CD]: """Returns the chat_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: - :obj:`defaultdict`: The restored chat data. + DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data. """ if self.chat_data: pass @@ -240,23 +218,20 @@ def get_chat_data(self) -> Mapping[int, CD]: filename = f"{self.filename}_chat_data" data = self.load_file(filename) if not data: - data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] - self.context_customizer.chat_data - ) + data = defaultdict(self.context_customizer.chat_data) else: - data = self.context_customizer.chat_data_mapping( # type: ignore[call-arg] - self.context_customizer.chat_data, data - ) + data = defaultdict(self.context_customizer.chat_data, data) self.chat_data = data else: self.load_singlefile() return self.chat_data # type: ignore[return-value] def get_bot_data(self) -> BD: - """Returns the bot_data from the pickle file if it exists or an empty :obj:`dict`. + """Returns the bot_data from the pickle file if it exists or an empty object of type + :class:`telegram.ext.utils.types.BD`. Returns: - :obj:`dict`: The restored bot data. + :class:`telegram.ext.utils.types.BD`: The restored user data. """ if self.bot_data: pass @@ -319,15 +294,11 @@ def update_user_data(self, user_id: int, data: UD) -> None: Args: user_id (:obj:`int`): The user the data might have been changed for. - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. + data (:class:`telegram.ext.utils.types.UD`): The + :attr:`telegram.ext.dispatcher.user_data` ``[user_id]``. """ if self.user_data is None: - self.user_data = cast( - MutableMapping[int, UD], - self.context_customizer.user_data_mapping( # type: ignore[call-arg] - self.context_customizer.user_data - ), - ) + self.user_data = defaultdict(self.context_customizer.user_data) if self.user_data.get(user_id) == data: return self.user_data[user_id] = data @@ -343,15 +314,11 @@ def update_chat_data(self, chat_id: int, data: CD) -> None: Args: chat_id (:obj:`int`): The chat the data might have been changed for. - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. + data (:class:`telegram.ext.utils.types.CD`): The + :attr:`telegram.ext.dispatcher.chat_data` ``[chat_id]``. """ if self.chat_data is None: - self.chat_data = cast( - MutableMapping[int, CD], - self.context_customizer.chat_data_mapping( # type: ignore[call-arg] - self.context_customizer.chat_data - ), - ) + self.chat_data = defaultdict(self.context_customizer.chat_data) if self.chat_data.get(chat_id) == data: return self.chat_data[chat_id] = data @@ -366,7 +333,8 @@ def update_bot_data(self, data: BD) -> None: """Will update the bot_data and depending on :attr:`on_flush` save the pickle file. Args: - data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data`. + data (:class:`telegram.ext.utils.types.BD`): The + :attr:`telegram.ext.dispatcher.bot_data`. """ if self.bot_data == data: return @@ -378,6 +346,24 @@ def update_bot_data(self, data: BD) -> None: else: self.dump_singlefile() + def refresh_user_data(self, user_id: int, user_data: UD) -> None: + """Does nothing. + + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` + """ + + def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: + """Does nothing. + + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` + """ + + def refresh_bot_data(self, bot_data: BD) -> None: + """Does nothing. + + .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` + """ + def flush(self) -> None: """Will save all data in memory to pickle file(s).""" if self.single_file: diff --git a/telegram/ext/pollanswerhandler.py b/telegram/ext/pollanswerhandler.py index e8a23ca6286..80c174f13a6 100644 --- a/telegram/ext/pollanswerhandler.py +++ b/telegram/ext/pollanswerhandler.py @@ -22,7 +22,7 @@ from telegram import Update from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT class PollAnswerHandler(Handler[Update, CCT]): diff --git a/telegram/ext/pollhandler.py b/telegram/ext/pollhandler.py index 9698185997c..01b16fb7174 100644 --- a/telegram/ext/pollhandler.py +++ b/telegram/ext/pollhandler.py @@ -22,7 +22,7 @@ from telegram import Update from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT class PollHandler(Handler[Update, CCT]): diff --git a/telegram/ext/precheckoutqueryhandler.py b/telegram/ext/precheckoutqueryhandler.py index 1f2750abbd3..19c25b92980 100644 --- a/telegram/ext/precheckoutqueryhandler.py +++ b/telegram/ext/precheckoutqueryhandler.py @@ -22,7 +22,7 @@ from telegram import Update from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT class PreCheckoutQueryHandler(Handler[Update, CCT]): diff --git a/telegram/ext/regexhandler.py b/telegram/ext/regexhandler.py index 3fd9c87a285..e4ace39d697 100644 --- a/telegram/ext/regexhandler.py +++ b/telegram/ext/regexhandler.py @@ -26,7 +26,7 @@ from telegram.ext import Filters, MessageHandler from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE -from telegram.utils.types import CCT +from telegram.ext.utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher diff --git a/telegram/ext/shippingqueryhandler.py b/telegram/ext/shippingqueryhandler.py index 7c36f702d82..116d1ab950a 100644 --- a/telegram/ext/shippingqueryhandler.py +++ b/telegram/ext/shippingqueryhandler.py @@ -21,7 +21,7 @@ from telegram import Update from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT class ShippingQueryHandler(Handler[Update, CCT]): diff --git a/telegram/ext/stringcommandhandler.py b/telegram/ext/stringcommandhandler.py index a4216f052e5..e2d92a726e4 100644 --- a/telegram/ext/stringcommandhandler.py +++ b/telegram/ext/stringcommandhandler.py @@ -23,7 +23,7 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher diff --git a/telegram/ext/stringregexhandler.py b/telegram/ext/stringregexhandler.py index b0465f6fc38..40775cbafac 100644 --- a/telegram/ext/stringregexhandler.py +++ b/telegram/ext/stringregexhandler.py @@ -24,7 +24,7 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher diff --git a/telegram/ext/typehandler.py b/telegram/ext/typehandler.py index 3a745b40b71..071bbf892ae 100644 --- a/telegram/ext/typehandler.py +++ b/telegram/ext/typehandler.py @@ -22,7 +22,7 @@ from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler -from ..utils.types import CCT +from .utils.types import CCT RT = TypeVar('RT') UT = TypeVar('UT') diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 127e9950624..f559994c2d1 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -41,19 +41,18 @@ from telegram import Bot, TelegramError from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized -from telegram.ext import Dispatcher, JobQueue, ContextCustomizer +from telegram.ext import Dispatcher, JobQueue, ContextTypes from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import get_signal_name from telegram.utils.request import Request -from telegram.utils.types import CCT, UD, BD, UDM, CDM, CD +from telegram.ext.utils.types import CCT, UD, CD, BD from telegram.ext.utils.webhookhandler import WebhookAppClass, WebhookServer if TYPE_CHECKING: - from collections import defaultdict from telegram.ext import BasePersistence, Defaults, CallbackContext -class Updater(Generic[CCT, UD, CD, BD, UDM, CDM]): +class Updater(Generic[CCT, UD, CD, BD]): """ This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to @@ -99,10 +98,10 @@ class Updater(Generic[CCT, UD, CD, BD, UDM, CDM]): used). defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to be used if not set explicitly in the bot methods. - context_customizer (:class:`telegram.ext.ContextCustomizer`, optional): Pass an instance - of :class:`telegram.ext.ContextCustomizer` to customize the the types used in the + context_customizer (:class:`telegram.ext.ContextTypes`, optional): Pass an instance + of :class:`telegram.ext.ContextTypes` to customize the the types used in the ``context`` interface. If not passed, the defaults documented in - :class:`telegram.ext.ContextCustomizer` will be used. + :class:`telegram.ext.ContextTypes` will be used. Raises: ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them. @@ -127,7 +126,7 @@ class Updater(Generic[CCT, UD, CD, BD, UDM, CDM]): @overload def __init__( - self: 'Updater[CallbackContext, dict, dict, dict, defaultdict, defaultdict]', + self: 'Updater[CallbackContext, dict, dict, dict]', token: str = None, base_url: str = None, workers: int = 4, @@ -145,7 +144,7 @@ def __init__( @overload def __init__( - self: 'Updater[CCT, UD, CD, BD, UDM, CDM]', + self: 'Updater[CCT, UD, CD, BD]', token: str = None, base_url: str = None, workers: int = 4, @@ -158,15 +157,15 @@ def __init__( defaults: 'Defaults' = None, use_context: bool = True, base_file_url: str = None, - context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, + context_customizer: ContextTypes[CCT, UD, CD, BD] = None, ): ... @overload def __init__( - self: 'Updater[CCT, UD, CD, BD, UDM, CDM]', + self: 'Updater[CCT, UD, CD, BD]', user_sig_handler: Callable = None, - dispatcher: Dispatcher[CCT, UD, CD, BD, UDM, CDM] = None, + dispatcher: Dispatcher[CCT, UD, CD, BD] = None, ): ... @@ -185,7 +184,7 @@ def __init__( # type: ignore[no-untyped-def,misc] use_context: bool = True, dispatcher=None, base_file_url: str = None, - context_customizer: ContextCustomizer[CCT, UD, CD, BD, UDM, CDM] = None, + context_customizer: ContextTypes[CCT, UD, CD, BD] = None, ): if defaults and bot: diff --git a/telegram/ext/utils/types.py b/telegram/ext/utils/types.py new file mode 100644 index 00000000000..4769d1fe28a --- /dev/null +++ b/telegram/ext/utils/types.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains custom typing aliases.""" +from typing import TypeVar, TYPE_CHECKING + +if TYPE_CHECKING: + from telegram.ext import CallbackContext # noqa: F401 + +CCT = TypeVar('CCT', bound='CallbackContext') +"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.""" +UD = TypeVar('UD') +"""Type of the user data for a single user.""" +CD = TypeVar('CD') +"""Type of the chat data for a single user.""" +BD = TypeVar('BD') +"""Type of the bot data.""" diff --git a/telegram/utils/types.py b/telegram/utils/types.py index b056dcdf05c..1ffcb2e44ba 100644 --- a/telegram/utils/types.py +++ b/telegram/utils/types.py @@ -28,14 +28,11 @@ Tuple, TypeVar, Union, - Mapping, - DefaultDict, ) if TYPE_CHECKING: from telegram import InputFile # noqa: F401 from telegram.utils.helpers import DefaultValue # noqa: F401 - from telegram.ext import CallbackContext # noqa: F401 FileLike = Union[IO, 'InputFile'] """Either an open file handler or a :class:`telegram.InputFile`.""" @@ -61,21 +58,3 @@ RT = TypeVar("RT") SLT = Union[RT, List[RT], Tuple[RT, ...]] """Single instance or list/tuple of instances.""" - -CCT = TypeVar('CCT', bound='CallbackContext') -"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.""" - -UD = TypeVar('UD') -"""Type of the user data for a single user.""" -CD = TypeVar('CD') -"""Type of the chat data for a single user.""" -BD = TypeVar('BD') -"""Type of the bot data.""" -UDM = TypeVar('UDM', bound=Mapping) -"""Type of the user data mapping.""" -CDM = TypeVar('CDM', bound=Mapping) -"""Type of the chat data mapping.""" - -DDType = TypeVar('DDType') -IntDD = DefaultDict[int, DDType] -"""Type for default dicts with integer keys and generic value.""" diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 191c9f607c8..6ec9fc08614 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -32,7 +32,7 @@ CallbackContext, JobQueue, BasePersistence, - ContextCustomizer, + ContextTypes, ) from telegram.ext.dispatcher import run_async, Dispatcher, DispatcherHandlerStop from telegram.utils.deprecate import TelegramDeprecationWarning @@ -918,7 +918,7 @@ class CustomUserMapping(defaultdict): class CustomChatMapping(defaultdict): pass - cc = ContextCustomizer( + cc = ContextTypes( context=CustomContext, user_data=int, chat_data=float, @@ -947,7 +947,7 @@ def error_handler(_, context): dispatcher = Dispatcher( bot, Queue(), - context_customizer=ContextCustomizer( + context_customizer=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) @@ -970,7 +970,7 @@ def callback(_, context): dispatcher = Dispatcher( bot, Queue(), - context_customizer=ContextCustomizer( + context_customizer=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index a665672c208..fa9f5690141 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -29,7 +29,7 @@ import pytz from apscheduler.schedulers import SchedulerNotRunningError from flaky import flaky -from telegram.ext import JobQueue, Updater, Job, CallbackContext, Dispatcher, ContextCustomizer +from telegram.ext import JobQueue, Updater, Job, CallbackContext, Dispatcher, ContextTypes class CustomContext(CallbackContext): @@ -520,7 +520,7 @@ def test_custom_context(self, bot, job_queue): dispatcher = Dispatcher( bot, Queue(), - context_customizer=ContextCustomizer( + context_customizer=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6782bd98db9..7e0b71c1356 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -45,7 +45,7 @@ DictPersistence, TypeHandler, JobQueue, - ContextCustomizer, + ContextTypes, ) @@ -1388,7 +1388,7 @@ def job_callback(context): @pytest.mark.parametrize('cd', [int, float, complex]) @pytest.mark.parametrize('bd', [int, float, complex]) def test_with_context_customizer(self, ud, cd, bd, udm, cdm, singlefile): - cc = ContextCustomizer( + cc = ContextTypes( user_data=ud, chat_data=cd, bot_data=bd, user_data_mapping=udm, chat_data_mapping=cdm ) persistence = PicklePersistence( From 524ce0dc75afb68496fb542b5f55484c4da8cfc1 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 30 Apr 2021 17:59:28 +0200 Subject: [PATCH 13/31] Fix existing tests --- telegram/ext/basepersistence.py | 12 +++++++++--- tests/test_dispatcher.py | 28 ++++++++++++++++++---------- tests/test_persistence.py | 22 ++++++++++------------ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 436d5125ae7..b4318fb7a00 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -391,7 +391,9 @@ def refresh_user_data(self, user_id: int, user_data: UD) -> None: user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with. user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user. """ - raise NotImplementedError + raise NotImplementedError( + 'refresh_user_data ist not implemented for this persistence class.' + ) def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` before passing the @@ -402,7 +404,9 @@ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with. chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat. """ - raise NotImplementedError + raise NotImplementedError( + 'refresh_chat_data ist not implemented for this persistence class.' + ) def refresh_bot_data(self, bot_data: BD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` before passing the @@ -412,7 +416,9 @@ def refresh_bot_data(self, bot_data: BD) -> None: Args: bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``. """ - raise NotImplementedError + raise NotImplementedError( + 'refresh_bot_data ist not implemented for this persistence class.' + ) def flush(self) -> None: """Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 6ec9fc08614..72ab79db92b 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -734,6 +734,15 @@ def get_conversations(self, name): def update_conversation(self, name, key, new_state): pass + def refresh_bot_data(self, bot_data): + pass + + def refresh_user_data(self, user_id, user_data): + pass + + def refresh_chat_data(self, chat_id, chat_data): + pass + def callback(update, context): pass @@ -794,6 +803,15 @@ def get_bot_data(self): def get_chat_data(self): pass + def refresh_bot_data(self, bot_data): + pass + + def refresh_user_data(self, user_id, user_data): + pass + + def refresh_chat_data(self, chat_id, chat_data): + pass + def callback(update, context): pass @@ -912,26 +930,16 @@ def dummy_callback(*args, **kwargs): dp.bot.defaults = None def test_custom_context_init(self, bot): - class CustomUserMapping(defaultdict): - pass - - class CustomChatMapping(defaultdict): - pass - cc = ContextTypes( context=CustomContext, user_data=int, chat_data=float, bot_data=complex, - user_data_mapping=CustomUserMapping, - chat_data_mapping=CustomChatMapping, ) dispatcher = Dispatcher(bot, Queue(), context_customizer=cc) - assert isinstance(dispatcher.user_data, CustomUserMapping) assert isinstance(dispatcher.user_data[1], int) - assert isinstance(dispatcher.chat_data, CustomChatMapping) assert isinstance(dispatcher.chat_data[1], float) assert isinstance(dispatcher.bot_data, complex) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 7e0b71c1356..bf3886aee58 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -256,8 +256,10 @@ def get_bot_data(): base_persistence.get_user_data = get_user_data base_persistence.get_chat_data = get_chat_data base_persistence.get_bot_data = get_bot_data - # base_persistence.update_chat_data = lambda x: x - # base_persistence.update_user_data = lambda x: x + base_persistence.refresh_bot_data = lambda x: x + base_persistence.refresh_chat_data = lambda x, y: x + base_persistence.refresh_user_data = lambda x, y: x + updater = Updater(bot=bot, persistence=base_persistence, use_context=True) dp = updater.dispatcher @@ -362,6 +364,10 @@ def get_bot_data(): base_persistence.get_user_data = get_user_data base_persistence.get_chat_data = get_chat_data base_persistence.get_bot_data = get_bot_data + base_persistence.refresh_bot_data = lambda x: x + base_persistence.refresh_chat_data = lambda x, y: x + base_persistence.refresh_user_data = lambda x, y: x + cdp.persistence = base_persistence cdp.user_data = user_data cdp.chat_data = chat_data @@ -1382,23 +1388,17 @@ def job_callback(context): assert user_data[789] == {'test3': '123'} @pytest.mark.parametrize('singlefile', [True, False]) - @pytest.mark.parametrize('udm', [defaultdict, CustomMapping]) - @pytest.mark.parametrize('cdm', [defaultdict, CustomMapping]) @pytest.mark.parametrize('ud', [int, float, complex]) @pytest.mark.parametrize('cd', [int, float, complex]) @pytest.mark.parametrize('bd', [int, float, complex]) - def test_with_context_customizer(self, ud, cd, bd, udm, cdm, singlefile): - cc = ContextTypes( - user_data=ud, chat_data=cd, bot_data=bd, user_data_mapping=udm, chat_data_mapping=cdm - ) + def test_with_context_customizer(self, ud, cd, bd, singlefile): + cc = ContextTypes(user_data=ud, chat_data=cd, bot_data=bd) persistence = PicklePersistence( 'pickletest', single_file=singlefile, context_customizer=cc ) - assert isinstance(persistence.get_user_data(), udm) assert isinstance(persistence.get_user_data()[1], ud) assert persistence.get_user_data()[1] == 0 - assert isinstance(persistence.get_chat_data(), cdm) assert isinstance(persistence.get_chat_data()[1], cd) assert persistence.get_chat_data()[1] == 0 assert isinstance(persistence.get_bot_data(), bd) @@ -1417,10 +1417,8 @@ def test_with_context_customizer(self, ud, cd, bd, udm, cdm, singlefile): persistence = PicklePersistence( 'pickletest', single_file=singlefile, context_customizer=cc ) - assert isinstance(persistence.get_user_data(), udm) assert isinstance(persistence.get_user_data()[1], ud) assert persistence.get_user_data()[1] == 1 - assert isinstance(persistence.get_chat_data(), cdm) assert isinstance(persistence.get_chat_data()[1], cd) assert persistence.get_chat_data()[1] == 1 assert isinstance(persistence.get_bot_data(), bd) From a0a65dcf041db52afbbf0ae346e026eaea5aab09 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 30 Apr 2021 19:21:33 +0200 Subject: [PATCH 14/31] Add new tests --- telegram/ext/basepersistence.py | 3 +- tests/test_callbackcontext.py | 4 + tests/test_contexttypes.py | 47 ++++++ tests/test_persistence.py | 254 +++++++++++++++++--------------- 4 files changed, 189 insertions(+), 119 deletions(-) create mode 100644 tests/test_contexttypes.py diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index b4318fb7a00..900fa98ef2a 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -48,7 +48,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC): * :meth:`flush` If you don't actually need one of those methods, a simple ``pass`` is enough. For example, if - ``store_bot_data=False``, you don't need :meth:`get_bot_data` and :meth:`update_bot_data`. + ``store_bot_data=False``, you don't need :meth:`get_bot_data`, :meth:`update_bot_data` or + :meth:`refresh_bot_data`. Warning: Persistence will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and diff --git a/tests/test_callbackcontext.py b/tests/test_callbackcontext.py index 8018b0ce0d4..8c114a3e200 100644 --- a/tests/test_callbackcontext.py +++ b/tests/test_callbackcontext.py @@ -21,6 +21,10 @@ from telegram import Update, Message, Chat, User, TelegramError from telegram.ext import CallbackContext +""" +CallbackContext.refresh_data is tested in TestBasePersistence +""" + class TestCallbackContext: def test_non_context_dp(self, dp): diff --git a/tests/test_contexttypes.py b/tests/test_contexttypes.py new file mode 100644 index 00000000000..a6cfeb587b6 --- /dev/null +++ b/tests/test_contexttypes.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# 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 pytest + +from telegram.ext import ContextTypes, CallbackContext + + +class SubClass(CallbackContext): + pass + + +class TestContextTypes: + def test_data_init(self): + ct = ContextTypes(SubClass, int, float, bool) + assert ct.context is SubClass + assert ct.bot_data is int + assert ct.chat_data is float + assert ct.user_data is bool + + with pytest.raises(ValueError, match='subclass of CallbackContext'): + ContextTypes(context=bool) + + def test_data_assignment(self): + ct = ContextTypes() + + with pytest.raises(AttributeError): + ct.bot_data = bool + with pytest.raises(AttributeError): + ct.user_data = bool + with pytest.raises(AttributeError): + ct.chat_data = bool diff --git a/tests/test_persistence.py b/tests/test_persistence.py index bf3886aee58..5f56b73ef10 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -170,6 +170,12 @@ def job_queue(bot): class TestBasePersistence: + test_flag = False + + @pytest.fixture(scope='function', autouse=True) + def reset(self): + self.test_flag = False + def test_creation(self, base_persistence): assert base_persistence.store_chat_data assert base_persistence.store_user_data @@ -186,6 +192,14 @@ def test_abstract_methods(self): ): BasePersistence() + def test_not_implemented_errors(self, base_persistence): + with pytest.raises(NotImplementedError, match='refresh_bot_data'): + base_persistence.refresh_bot_data(True) + with pytest.raises(NotImplementedError, match='refresh_chat_data'): + base_persistence.refresh_chat_data(1, True) + with pytest.raises(NotImplementedError, match='refresh_user_data'): + base_persistence.refresh_user_data(1, True) + def test_implementation(self, updater, base_persistence): dp = updater.dispatcher assert dp.persistence == base_persistence @@ -241,116 +255,17 @@ def get_bot_data(): u.dispatcher.chat_data[442233]['test5'] = 'test6' assert u.dispatcher.chat_data[442233]['test5'] == 'test6' + @pytest.mark.parametrize('run_async', [True, False], ids=['synchronous', 'run_async']) def test_dispatcher_integration_handlers( - self, caplog, bot, base_persistence, chat_data, user_data, bot_data - ): - def get_user_data(): - return user_data - - def get_chat_data(): - return chat_data - - def get_bot_data(): - return bot_data - - base_persistence.get_user_data = get_user_data - base_persistence.get_chat_data = get_chat_data - base_persistence.get_bot_data = get_bot_data - base_persistence.refresh_bot_data = lambda x: x - base_persistence.refresh_chat_data = lambda x, y: x - base_persistence.refresh_user_data = lambda x, y: x - - updater = Updater(bot=bot, persistence=base_persistence, use_context=True) - dp = updater.dispatcher - - def callback_known_user(update, context): - if not context.user_data['test1'] == 'test2': - pytest.fail('user_data corrupt') - if not context.bot_data == bot_data: - pytest.fail('bot_data corrupt') - - def callback_known_chat(update, context): - if not context.chat_data['test3'] == 'test4': - pytest.fail('chat_data corrupt') - if not context.bot_data == bot_data: - pytest.fail('bot_data corrupt') - - def callback_unknown_user_or_chat(update, context): - if not context.user_data == {}: - pytest.fail('user_data corrupt') - if not context.chat_data == {}: - pytest.fail('chat_data corrupt') - if not context.bot_data == bot_data: - pytest.fail('bot_data corrupt') - context.user_data[1] = 'test7' - context.chat_data[2] = 'test8' - context.bot_data['test0'] = 'test0' - - known_user = MessageHandler( - Filters.user(user_id=12345), - callback_known_user, - pass_chat_data=True, - pass_user_data=True, - ) - known_chat = MessageHandler( - Filters.chat(chat_id=-67890), - callback_known_chat, - pass_chat_data=True, - pass_user_data=True, - ) - unknown = MessageHandler( - Filters.all, callback_unknown_user_or_chat, pass_chat_data=True, pass_user_data=True - ) - dp.add_handler(known_user) - dp.add_handler(known_chat) - dp.add_handler(unknown) - user1 = User(id=12345, first_name='test user', is_bot=False) - user2 = User(id=54321, first_name='test user', is_bot=False) - chat1 = Chat(id=-67890, type='group') - chat2 = Chat(id=-987654, type='group') - m = Message(1, None, chat2, from_user=user1) - u = Update(0, m) - with caplog.at_level(logging.ERROR): - dp.process_update(u) - rec = caplog.records[-1] - assert rec.getMessage() == 'No error handlers are registered, logging exception.' - assert rec.levelname == 'ERROR' - rec = caplog.records[-2] - assert rec.getMessage() == 'No error handlers are registered, logging exception.' - assert rec.levelname == 'ERROR' - rec = caplog.records[-3] - assert rec.getMessage() == 'No error handlers are registered, logging exception.' - assert rec.levelname == 'ERROR' - m.from_user = user2 - m.chat = chat1 - u = Update(1, m) - dp.process_update(u) - m.chat = chat2 - u = Update(2, m) - - def save_bot_data(data): - if 'test0' not in data: - pytest.fail() - - def save_chat_data(data): - if -987654 not in data: - pytest.fail() - - def save_user_data(data): - if 54321 not in data: - pytest.fail() - - base_persistence.update_chat_data = save_chat_data - base_persistence.update_user_data = save_user_data - base_persistence.update_bot_data = save_bot_data - dp.process_update(u) - - assert dp.user_data[54321][1] == 'test7' - assert dp.chat_data[-987654][2] == 'test8' - assert dp.bot_data['test0'] == 'test0' - - def test_dispatcher_integration_handlers_run_async( - self, cdp, caplog, bot, base_persistence, chat_data, user_data, bot_data + self, + cdp, + caplog, + bot, + base_persistence, + chat_data, + user_data, + bot_data, + run_async, ): def get_user_data(): return user_data @@ -401,21 +316,21 @@ def callback_unknown_user_or_chat(update, context): callback_known_user, pass_chat_data=True, pass_user_data=True, - run_async=True, + run_async=run_async, ) known_chat = MessageHandler( Filters.chat(chat_id=-67890), callback_known_chat, pass_chat_data=True, pass_user_data=True, - run_async=True, + run_async=run_async, ) unknown = MessageHandler( Filters.all, callback_unknown_user_or_chat, pass_chat_data=True, pass_user_data=True, - run_async=True, + run_async=run_async, ) cdp.add_handler(known_user) cdp.add_handler(known_chat) @@ -430,12 +345,16 @@ def callback_unknown_user_or_chat(update, context): cdp.process_update(u) sleep(0.1) - rec = caplog.records[-1] - assert rec.getMessage() == 'No error handlers are registered, logging exception.' - assert rec.levelname == 'ERROR' - rec = caplog.records[-2] - assert rec.getMessage() == 'No error handlers are registered, logging exception.' - assert rec.levelname == 'ERROR' + + # In base_persistence.update_*_data we currently just raise NotImplementedError + # This makes sure that this doesn't break the processing and is properly handled by + # the error handler + # We override `update_*_data` further below. + assert len(caplog.records) == 3 + for rec in caplog.records: + assert rec.getMessage() == 'No error handlers are registered, logging exception.' + assert rec.levelname == 'ERROR' + m.from_user = user2 m.chat = chat1 u = Update(1, m) @@ -466,6 +385,105 @@ def save_user_data(data): assert cdp.chat_data[-987654][2] == 'test8' assert cdp.bot_data['test0'] == 'test0' + @pytest.mark.parametrize( + 'store_user_data', [True, False], ids=['store_user_data-True', 'store_user_data-False'] + ) + @pytest.mark.parametrize( + 'store_chat_data', [True, False], ids=['store_chat_data-True', 'store_chat_data-False'] + ) + @pytest.mark.parametrize( + 'store_bot_data', [True, False], ids=['store_bot_data-True', 'store_bot_data-False'] + ) + @pytest.mark.parametrize('run_async', [True, False], ids=['synchronous', 'run_async']) + def test_persistence_dispatcher_integration_refresh_data( + self, + cdp, + base_persistence, + chat_data, + bot_data, + user_data, + store_bot_data, + store_chat_data, + store_user_data, + run_async, + ): + base_persistence.refresh_bot_data = lambda x: x.setdefault( + 'refreshed', x.get('refreshed', 0) + 1 + ) + # x is the user/chat_id + base_persistence.refresh_chat_data = lambda x, y: y.setdefault('refreshed', x) + base_persistence.refresh_user_data = lambda x, y: y.setdefault('refreshed', x) + base_persistence.store_bot_data = store_bot_data + base_persistence.store_chat_data = store_chat_data + base_persistence.store_user_data = store_user_data + cdp.persistence = base_persistence + + self.test_flag = True + + def callback_with_user_and_chat(update, context): + if store_user_data: + if context.user_data.get('refreshed') != update.effective_user.id: + self.test_flag = 'user_data was not refreshed' + else: + if 'refreshed' in context.user_data: + self.test_flag = 'user_data was wrongly refreshed' + if store_chat_data: + if context.chat_data.get('refreshed') != update.effective_chat.id: + self.test_flag = 'chat_data was not refreshed' + else: + if 'refreshed' in context.chat_data: + self.test_flag = 'chat_data was wrongly refreshed' + if store_bot_data: + if context.bot_data.get('refreshed') != 1: + self.test_flag = 'bot_data was not refreshed' + else: + if 'refreshed' in context.bot_data: + self.test_flag = 'bot_data was wrongly refreshed' + + def callback_without_user_and_chat(_, context): + if store_bot_data: + if context.bot_data.get('refreshed') != 1: + self.test_flag = 'bot_data was not refreshed' + else: + if 'refreshed' in context.bot_data: + self.test_flag = 'bot_data was wrongly refreshed' + + with_user_and_chat = MessageHandler( + Filters.user(user_id=12345), + callback_with_user_and_chat, + pass_chat_data=True, + pass_user_data=True, + run_async=run_async, + ) + without_user_and_chat = MessageHandler( + Filters.all, + callback_without_user_and_chat, + pass_chat_data=True, + pass_user_data=True, + run_async=run_async, + ) + cdp.add_handler(with_user_and_chat) + cdp.add_handler(without_user_and_chat) + user = User(id=12345, first_name='test user', is_bot=False) + chat = Chat(id=-987654, type='group') + m = Message(1, None, chat, from_user=user) + + # has user and chat + u = Update(0, m) + cdp.process_update(u) + + assert self.test_flag is True + + # has neither user nor hat + m.from_user = None + m.chat = None + u = Update(1, m) + cdp.process_update(u) + + assert self.test_flag is True + + sleep(0.1) + def test_persistence_dispatcher_arbitrary_update_types(self, dp, base_persistence, caplog): # Updates used with TypeHandler doesn't necessarily have the proper attributes for # persistence, makes sure it works anyways From 86f3a1308e383cc22bde956ca40b9237890d8d9e Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 1 May 2021 11:55:32 +0200 Subject: [PATCH 15/31] Update docs & add some versioning directives --- telegram/ext/contexttypes.py | 2 ++ telegram/ext/dispatcher.py | 34 ++++++++++++----------- telegram/ext/jobqueue.py | 4 +-- telegram/ext/picklepersistence.py | 46 ++++++++++++++++--------------- telegram/ext/updater.py | 14 ++++++---- tests/test_dispatcher.py | 6 ++-- tests/test_jobqueue.py | 2 +- tests/test_persistence.py | 10 ++----- tests/test_updater.py | 2 +- 9 files changed, 62 insertions(+), 58 deletions(-) diff --git a/telegram/ext/contexttypes.py b/telegram/ext/contexttypes.py index 10286e40c2e..f39f3ebce36 100644 --- a/telegram/ext/contexttypes.py +++ b/telegram/ext/contexttypes.py @@ -29,6 +29,8 @@ class ContextTypes(Generic[CCT, UD, CD, BD]): Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext` interface. + .. versionadded:: 13.6 + Args: context (:obj:`type`, optional): Determines the type of the ``context`` argument of all (error-)handler callbacks and job callbacks. Must be a subclass of diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index faf060908fd..e0e613f5d9e 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -134,10 +134,12 @@ class Dispatcher(Generic[CCT, UD, CD, BD]): use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. **New users**: set this to :obj:`True`. - context_customizer (:class:`telegram.ext.ContextCustomizer`, optional): Pass an instance - of :class:`telegram.ext.ContextCustomizer` to customize the the types used in the + context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance + of :class:`telegram.ext.ContextTypes` to customize the the types used in the ``context`` interface. If not passed, the defaults documented in - :class:`telegram.ext.ContextCustomizer` will be used. + :class:`telegram.ext.ContextTypes` will be used. + + .. versionadded:: 13.6 Attributes: bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. @@ -151,9 +153,11 @@ class Dispatcher(Generic[CCT, UD, CD, BD]): bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot. persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to store data that should be persistent over restarts. - context_customizer (:class:`telegram.ext.ContextTypes`): Container for the types used + context_types (:class:`telegram.ext.ContextTypes`): Container for the types used in the ``context`` interface. + .. versionadded:: 13.6 + """ __singleton_lock = Lock() @@ -184,7 +188,7 @@ def __init__( job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, - context_customizer: ContextTypes[CCT, UD, CD, BD] = None, + context_types: ContextTypes[CCT, UD, CD, BD] = None, ): ... @@ -197,16 +201,14 @@ def __init__( job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, - context_customizer: ContextTypes[CCT, UD, CD, BD] = None, + context_types: ContextTypes[CCT, UD, CD, BD] = None, ): self.bot = bot self.update_queue = update_queue self.job_queue = job_queue self.workers = workers self.use_context = use_context - self.context_customizer = cast( - ContextTypes[CCT, UD, CD, BD], context_customizer or ContextTypes() - ) + self.context_types = cast(ContextTypes[CCT, UD, CD, BD], context_types or ContextTypes()) if not use_context: warnings.warn( @@ -220,9 +222,9 @@ def __init__( 'Asynchronous callbacks can not be processed without at least one worker thread.' ) - self.user_data: DefaultDict[int, UD] = defaultdict(self.context_customizer.user_data) - self.chat_data: DefaultDict[int, CD] = defaultdict(self.context_customizer.chat_data) - self.bot_data = self.context_customizer.bot_data() + self.user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data) + self.chat_data: DefaultDict[int, CD] = defaultdict(self.context_types.chat_data) + self.bot_data = self.context_types.bot_data() self.persistence: Optional[BasePersistence] = None self._update_persistence_lock = Lock() if persistence: @@ -240,9 +242,9 @@ def __init__( raise ValueError("chat_data must be of type defaultdict") if self.persistence.store_bot_data: self.bot_data = self.persistence.get_bot_data() - if not isinstance(self.bot_data, self.context_customizer.bot_data): + if not isinstance(self.bot_data, self.context_types.bot_data): raise ValueError( - f"bot_data must be of type {self.context_customizer.bot_data.__name__}" + f"bot_data must be of type {self.context_types.bot_data.__name__}" ) else: @@ -497,7 +499,7 @@ def process_update(self, update: object) -> None: check = handler.check_update(update) if check is not None and check is not False: if not context and self.use_context: - context = self.context_customizer.context.from_update(update, self) + context = self.context_types.context.from_update(update, self) context.refresh_data() handled = True sync_modes.append(handler.run_async) @@ -735,7 +737,7 @@ def dispatch_error( if self.error_handlers: for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621 if self.use_context: - context = self.context_customizer.context.from_error( + context = self.context_types.context.from_error( update, error, self, async_args=async_args, async_kwargs=async_kwargs ) if run_async: diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index 6b88bda65c8..1ff8e68a1ef 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -71,7 +71,7 @@ def aps_log_filter(record): # type: ignore def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]: if self._dispatcher.use_context: - return [self._dispatcher.context_customizer.context.from_job(job, self._dispatcher)] + return [self._dispatcher.context_types.context.from_job(job, self._dispatcher)] return [self._dispatcher.bot, job] def _tz_now(self) -> datetime.datetime: @@ -569,7 +569,7 @@ def run(self, dispatcher: 'Dispatcher') -> None: """Executes the callback function independently of the jobs schedule.""" try: if dispatcher.use_context: - self.callback(dispatcher.context_customizer.context.from_job(self, dispatcher)) + self.callback(dispatcher.context_types.context.from_job(self, dispatcher)) else: self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg] except Exception as exc: diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index 3586c386210..2a1eeb3b9fa 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -63,10 +63,12 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): :meth:`flush` is called and keep data in memory until that happens. When :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. - context_customizer (:class:`telegram.ext.ContextCustomizer`, optional): Pass an instance - of :class:`telegram.ext.ContextCustomizer` to customize the the types used in the + context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance + of :class:`telegram.ext.ContextTypes` to customize the the types used in the ``context`` interface. If not passed, the defaults documented in - :class:`telegram.ext.ContextCustomizer` will be used. + :class:`telegram.ext.ContextTypes` will be used. + + .. versionadded:: 13.6 Attributes: filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` @@ -84,8 +86,10 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): :meth:`flush` is called and keep data in memory until that happens. When :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. - context_customizer (:class:`telegram.ext.ContextTypes`): Container for the types used + context_types (:class:`telegram.ext.ContextTypes`): Container for the types used in the ``context`` interface. + + .. versionadded:: 13.6 """ @overload @@ -109,7 +113,7 @@ def __init__( store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, - context_customizer: ContextTypes[Any, UD, CD, BD] = None, + context_types: ContextTypes[Any, UD, CD, BD] = None, ): ... @@ -121,7 +125,7 @@ def __init__( store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, - context_customizer: ContextTypes[Any, UD, CD, BD] = None, + context_types: ContextTypes[Any, UD, CD, BD] = None, ): super().__init__( store_user_data=store_user_data, @@ -135,25 +139,23 @@ def __init__( self.chat_data: Optional[DefaultDict[int, CD]] = None self.bot_data: Optional[BD] = None self.conversations: Optional[Dict[str, Dict[Tuple, object]]] = None - self.context_customizer = cast( - ContextTypes[Any, UD, CD, BD], context_customizer or ContextTypes() - ) + self.context_types = cast(ContextTypes[Any, UD, CD, BD], context_types or ContextTypes()) def load_singlefile(self) -> None: try: filename = self.filename with open(self.filename, "rb") as file: data = pickle.load(file) - self.user_data = defaultdict(self.context_customizer.user_data, data['user_data']) - self.chat_data = defaultdict(self.context_customizer.chat_data, data['chat_data']) + self.user_data = defaultdict(self.context_types.user_data, data['user_data']) + self.chat_data = defaultdict(self.context_types.chat_data, data['chat_data']) # For backwards compatibility with files not containing bot data - self.bot_data = data.get('bot_data', self.context_customizer.bot_data()) + self.bot_data = data.get('bot_data', self.context_types.bot_data()) self.conversations = data['conversations'] except OSError: self.conversations = {} - self.user_data = defaultdict(self.context_customizer.user_data) - self.chat_data = defaultdict(self.context_customizer.chat_data) - self.bot_data = self.context_customizer.bot_data() + self.user_data = defaultdict(self.context_types.user_data) + self.chat_data = defaultdict(self.context_types.chat_data) + self.bot_data = self.context_types.bot_data() except pickle.UnpicklingError as exc: raise TypeError(f"File {filename} does not contain valid pickle data") from exc except Exception as exc: @@ -198,9 +200,9 @@ def get_user_data(self) -> DefaultDict[int, UD]: filename = f"{self.filename}_user_data" data = self.load_file(filename) if not data: - data = defaultdict(self.context_customizer.user_data) + data = defaultdict(self.context_types.user_data) else: - data = defaultdict(self.context_customizer.user_data, data) + data = defaultdict(self.context_types.user_data, data) self.user_data = data else: self.load_singlefile() @@ -218,9 +220,9 @@ def get_chat_data(self) -> DefaultDict[int, CD]: filename = f"{self.filename}_chat_data" data = self.load_file(filename) if not data: - data = defaultdict(self.context_customizer.chat_data) + data = defaultdict(self.context_types.chat_data) else: - data = defaultdict(self.context_customizer.chat_data, data) + data = defaultdict(self.context_types.chat_data, data) self.chat_data = data else: self.load_singlefile() @@ -239,7 +241,7 @@ def get_bot_data(self) -> BD: filename = f"{self.filename}_bot_data" data = self.load_file(filename) if not data: - data = self.context_customizer.bot_data() + data = self.context_types.bot_data() self.bot_data = data else: self.load_singlefile() @@ -298,7 +300,7 @@ def update_user_data(self, user_id: int, data: UD) -> None: :attr:`telegram.ext.dispatcher.user_data` ``[user_id]``. """ if self.user_data is None: - self.user_data = defaultdict(self.context_customizer.user_data) + self.user_data = defaultdict(self.context_types.user_data) if self.user_data.get(user_id) == data: return self.user_data[user_id] = data @@ -318,7 +320,7 @@ def update_chat_data(self, chat_id: int, data: CD) -> None: :attr:`telegram.ext.dispatcher.chat_data` ``[chat_id]``. """ if self.chat_data is None: - self.chat_data = defaultdict(self.context_customizer.chat_data) + self.chat_data = defaultdict(self.context_types.chat_data) if self.chat_data.get(chat_id) == data: return self.chat_data[chat_id] = data diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index f559994c2d1..24acec454b3 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -98,11 +98,13 @@ class Updater(Generic[CCT, UD, CD, BD]): used). defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to be used if not set explicitly in the bot methods. - context_customizer (:class:`telegram.ext.ContextTypes`, optional): Pass an instance + context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance of :class:`telegram.ext.ContextTypes` to customize the the types used in the ``context`` interface. If not passed, the defaults documented in :class:`telegram.ext.ContextTypes` will be used. + .. versionadded:: 13.6 + Raises: ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them. @@ -157,7 +159,7 @@ def __init__( defaults: 'Defaults' = None, use_context: bool = True, base_file_url: str = None, - context_customizer: ContextTypes[CCT, UD, CD, BD] = None, + context_types: ContextTypes[CCT, UD, CD, BD] = None, ): ... @@ -184,7 +186,7 @@ def __init__( # type: ignore[no-untyped-def,misc] use_context: bool = True, dispatcher=None, base_file_url: str = None, - context_customizer: ContextTypes[CCT, UD, CD, BD] = None, + context_types: ContextTypes[CCT, UD, CD, BD] = None, ): if defaults and bot: @@ -209,8 +211,8 @@ def __init__( # type: ignore[no-untyped-def,misc] raise ValueError('`dispatcher` and `persistence` are mutually exclusive') if use_context != dispatcher.use_context: raise ValueError('`dispatcher` and `use_context` are mutually exclusive') - if context_customizer is not None: - raise ValueError('`dispatcher` and `context_customizer` are mutually exclusive') + if context_types is not None: + raise ValueError('`dispatcher` and `context_types` are mutually exclusive') if workers is not None: raise ValueError('`dispatcher` and `workers` are mutually exclusive') @@ -259,7 +261,7 @@ def __init__( # type: ignore[no-untyped-def,misc] exception_event=self.__exception_event, persistence=persistence, use_context=use_context, - context_customizer=context_customizer, + context_types=context_types, ) self.job_queue.set_dispatcher(self.dispatcher) else: diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 72ab79db92b..ba07be47403 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -937,7 +937,7 @@ def test_custom_context_init(self, bot): bot_data=complex, ) - dispatcher = Dispatcher(bot, Queue(), context_customizer=cc) + dispatcher = Dispatcher(bot, Queue(), context_types=cc) assert isinstance(dispatcher.user_data[1], int) assert isinstance(dispatcher.chat_data[1], float) @@ -955,7 +955,7 @@ def error_handler(_, context): dispatcher = Dispatcher( bot, Queue(), - context_customizer=ContextTypes( + context_types=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) @@ -978,7 +978,7 @@ def callback(_, context): dispatcher = Dispatcher( bot, Queue(), - context_customizer=ContextTypes( + context_types=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index fa9f5690141..c3c888d761a 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -520,7 +520,7 @@ def test_custom_context(self, bot, job_queue): dispatcher = Dispatcher( bot, Queue(), - context_customizer=ContextTypes( + context_types=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5f56b73ef10..8b9f49a7e40 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1409,11 +1409,9 @@ def job_callback(context): @pytest.mark.parametrize('ud', [int, float, complex]) @pytest.mark.parametrize('cd', [int, float, complex]) @pytest.mark.parametrize('bd', [int, float, complex]) - def test_with_context_customizer(self, ud, cd, bd, singlefile): + def test_with_context_types(self, ud, cd, bd, singlefile): cc = ContextTypes(user_data=ud, chat_data=cd, bot_data=bd) - persistence = PicklePersistence( - 'pickletest', single_file=singlefile, context_customizer=cc - ) + persistence = PicklePersistence('pickletest', single_file=singlefile, context_types=cc) assert isinstance(persistence.get_user_data()[1], ud) assert persistence.get_user_data()[1] == 0 @@ -1432,9 +1430,7 @@ def test_with_context_customizer(self, ud, cd, bd, singlefile): assert persistence.get_bot_data() == 1 persistence.flush() - persistence = PicklePersistence( - 'pickletest', single_file=singlefile, context_customizer=cc - ) + persistence = PicklePersistence('pickletest', single_file=singlefile, context_types=cc) assert isinstance(persistence.get_user_data()[1], ud) assert persistence.get_user_data()[1] == 1 assert isinstance(persistence.get_chat_data()[1], cd) diff --git a/tests/test_updater.py b/tests/test_updater.py index 030436e3b59..9a1f9a53baa 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -644,7 +644,7 @@ def test_mutual_exclude_use_context_dispatcher(self): def test_mutual_exclude_custom_context_dispatcher(self): dispatcher = Dispatcher(None, None) with pytest.raises(ValueError): - Updater(dispatcher=dispatcher, context_customizer=True) + Updater(dispatcher=dispatcher, context_types=True) def test_defaults_warning(self, bot): with pytest.warns(TelegramDeprecationWarning, match='no effect when a Bot is passed'): From 5dc7e1822f5bf97ed6558705879eda2d3bc47980 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Wed, 5 May 2021 21:27:29 +0200 Subject: [PATCH 16/31] Annotation fix --- telegram/ext/basepersistence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 900fa98ef2a..5c8055295f2 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -20,7 +20,7 @@ import warnings from abc import ABC, abstractmethod from copy import copy -from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, Mapping, DefaultDict +from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, DefaultDict from telegram import Bot @@ -91,10 +91,10 @@ def __new__( update_chat_data = instance.update_chat_data update_bot_data = instance.update_bot_data - def get_user_data_insert_bot() -> Mapping[int, UD]: + def get_user_data_insert_bot() -> DefaultDict[int, UD]: return instance.insert_bot(get_user_data()) - def get_chat_data_insert_bot() -> Mapping[int, CD]: + def get_chat_data_insert_bot() -> DefaultDict[int, CD]: return instance.insert_bot(get_chat_data()) def get_bot_data_insert_bot() -> BD: From 39d1e781f1178dbc98418fd0d45b5d7fafe3397a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Wed, 5 May 2021 21:35:14 +0200 Subject: [PATCH 17/31] Only specify major version of GH Actions --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba3c919eff0..f9dbe68851d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: shell: bash --noprofile --norc {0} - name: Submit coverage - uses: codecov/codecov-action@v1.0.13 + uses: codecov/codecov-action@v1 with: env_vars: OS,PYTHON name: ${{ matrix.os }}-${{ matrix.python-version }} @@ -79,7 +79,7 @@ jobs: run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -108,7 +108,7 @@ jobs: run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 6753bcfcf192646156d6294ef4049c56e7558c84 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Mon, 10 May 2021 11:37:09 +0200 Subject: [PATCH 18/31] Slight typing improvements --- telegram/ext/callbackcontext.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index a0cb08946c5..465f19bb6d8 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -19,7 +19,19 @@ # pylint: disable=R0201 """This module contains the CallbackContext class.""" from queue import Queue -from typing import TYPE_CHECKING, Dict, List, Match, NoReturn, Optional, Tuple, Union, Generic +from typing import ( + TYPE_CHECKING, + Dict, + List, + Match, + NoReturn, + Optional, + Tuple, + Union, + Generic, + Type, + TypeVar, +) from telegram import Update from telegram.ext.utils.types import UD, CD, BD @@ -28,6 +40,8 @@ from telegram import Bot from telegram.ext import Dispatcher, Job, JobQueue +CC = TypeVar('CC', bound='CallbackContext') + class CallbackContext(Generic[UD, CD, BD]): """ @@ -172,13 +186,13 @@ def refresh_data(self) -> None: @classmethod def from_error( - cls, + cls: Type[CC], update: object, error: Exception, dispatcher: 'Dispatcher', async_args: Union[List, Tuple] = None, async_kwargs: Dict[str, object] = None, - ) -> 'CallbackContext': + ) -> CC: """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error handlers. @@ -209,7 +223,7 @@ def from_error( return self @classmethod - def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackContext': + def from_update(cls: Type[CC], update: object, dispatcher: 'Dispatcher') -> CC: """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the handlers. @@ -244,7 +258,7 @@ def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackConte return self @classmethod - def from_job(cls, job: 'Job', dispatcher: 'Dispatcher') -> 'CallbackContext': + def from_job(cls: Type[CC], job: 'Job', dispatcher: 'Dispatcher') -> CC: """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a job callback. From b20cd09119b6d076c0ae81da0a230a86a54c6634 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 14 May 2021 21:42:48 +0200 Subject: [PATCH 19/31] Address review --- docs/source/telegram.ext.rst | 1 - telegram/ext/basepersistence.py | 10 ++++++++-- telegram/ext/contexttypes.py | 6 ++++-- telegram/ext/dispatcher.py | 2 +- telegram/ext/picklepersistence.py | 2 +- telegram/ext/updater.py | 2 +- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/source/telegram.ext.rst b/docs/source/telegram.ext.rst index a53c182fd70..04918e8e6a4 100644 --- a/docs/source/telegram.ext.rst +++ b/docs/source/telegram.ext.rst @@ -12,7 +12,6 @@ telegram.ext package telegram.ext.jobqueue telegram.ext.messagequeue telegram.ext.delayqueue - telegram.ext.callbackcontext telegram.ext.contexttypes telegram.ext.defaults diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 5c8055295f2..99c4e0701cc 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -321,7 +321,7 @@ def get_bot_data(self) -> BD: :class:`telegram.ext.utils.types.BD`. Returns: - :class:`telegram.ext.utils.types.BD`: The restored user data. + :class:`telegram.ext.utils.types.BD`: The restored bot data. """ @abstractmethod @@ -388,12 +388,14 @@ def refresh_user_data(self, user_id: int, user_data: UD) -> None: :attr:`user_data` to a callback. Can be used to update data stored in :attr:`user_data` from an external source. + .. versionadded:: 13.6 + Args: user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with. user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user. """ raise NotImplementedError( - 'refresh_user_data ist not implemented for this persistence class.' + 'refresh_user_data is not implemented for this persistence class.' ) def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: @@ -401,6 +403,8 @@ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: :attr:`chat_data` to a callback. Can be used to update data stored in :attr:`chat_data` from an external source. + .. versionadded:: 13.6 + Args: chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with. chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat. @@ -414,6 +418,8 @@ def refresh_bot_data(self, bot_data: BD) -> None: :attr:`bot_data` to a callback. Can be used to update data stored in :attr:`bot_data` from an external source. + .. versionadded:: 13.6 + Args: bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``. """ diff --git a/telegram/ext/contexttypes.py b/telegram/ext/contexttypes.py index f39f3ebce36..87a71770a9d 100644 --- a/telegram/ext/contexttypes.py +++ b/telegram/ext/contexttypes.py @@ -40,9 +40,11 @@ class ContextTypes(Generic[CCT, UD, CD, BD]): (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support instantiating without arguments. chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments. user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all - (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. + (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support + instantiating without arguments. """ diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index e0e613f5d9e..9931c097e9f 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -135,7 +135,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD]): API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. **New users**: set this to :obj:`True`. context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance - of :class:`telegram.ext.ContextTypes` to customize the the types used in the + of :class:`telegram.ext.ContextTypes` to customize the types used in the ``context`` interface. If not passed, the defaults documented in :class:`telegram.ext.ContextTypes` will be used. diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index 2a1eeb3b9fa..90b07c1877a 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -64,7 +64,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]): :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance - of :class:`telegram.ext.ContextTypes` to customize the the types used in the + of :class:`telegram.ext.ContextTypes` to customize the types used in the ``context`` interface. If not passed, the defaults documented in :class:`telegram.ext.ContextTypes` will be used. diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 8e0c433f0da..524a0e11309 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -99,7 +99,7 @@ class Updater(Generic[CCT, UD, CD, BD]): defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to be used if not set explicitly in the bot methods. context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance - of :class:`telegram.ext.ContextTypes` to customize the the types used in the + of :class:`telegram.ext.ContextTypes` to customize the types used in the ``context`` interface. If not passed, the defaults documented in :class:`telegram.ext.ContextTypes` will be used. From e1844435fdf085ce536193f401632d6ea7268a65 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 14 May 2021 21:56:50 +0200 Subject: [PATCH 20/31] More review --- telegram/ext/contexttypes.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/telegram/ext/contexttypes.py b/telegram/ext/contexttypes.py index 87a71770a9d..7718814856a 100644 --- a/telegram/ext/contexttypes.py +++ b/telegram/ext/contexttypes.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. # pylint: disable=R0201 """This module contains the auxiliary class ContextTypes.""" -from typing import Type, Any, NoReturn, Generic, overload, Dict # pylint: disable=W0611 +from typing import Type, Generic, overload, Dict # pylint: disable=W0611 from telegram.ext.callbackcontext import CallbackContext from telegram.ext.utils.types import CCT, UD, CD, BD @@ -168,6 +168,8 @@ def __init__( # type: ignore[no-untyped-def] if not issubclass(context, CallbackContext): raise ValueError('context must be a subclass of CallbackContext.') + # We make all those only accessible via properties because we don't currently support + # changing this at runtime, so overriding the attributes doesn't make sense self._context = context self._bot_data = bot_data self._chat_data = chat_data @@ -177,30 +179,14 @@ def __init__( # type: ignore[no-untyped-def] def context(self) -> Type[CCT]: return self._context - @context.setter - def context(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextTypes attributes.") - @property def bot_data(self) -> Type[BD]: return self._bot_data - @bot_data.setter - def bot_data(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextTypes attributes.") - @property def chat_data(self) -> Type[CD]: return self._chat_data - @chat_data.setter - def chat_data(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextTypes attributes.") - @property def user_data(self) -> Type[UD]: return self._user_data - - @user_data.setter - def user_data(self, value: Any) -> NoReturn: - raise AttributeError("You can not assign a new value to ContextTypes attributes.") From ce4738c4128570db03d120c74aa14cdb89a38b94 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 14 May 2021 22:14:55 +0200 Subject: [PATCH 21/31] Shame on me --- docs/source/telegram.ext.rst | 1 - telegram/ext/basepersistence.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/source/telegram.ext.rst b/docs/source/telegram.ext.rst index 04918e8e6a4..31695044691 100644 --- a/docs/source/telegram.ext.rst +++ b/docs/source/telegram.ext.rst @@ -7,7 +7,6 @@ telegram.ext package telegram.ext.dispatcher telegram.ext.dispatcherhandlerstop telegram.ext.callbackcontext - telegram.ext.defaults telegram.ext.job telegram.ext.jobqueue telegram.ext.messagequeue diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 99c4e0701cc..3e83b6594f2 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -410,7 +410,7 @@ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat. """ raise NotImplementedError( - 'refresh_chat_data ist not implemented for this persistence class.' + 'refresh_chat_data is not implemented for this persistence class.' ) def refresh_bot_data(self, bot_data: BD) -> None: @@ -424,7 +424,7 @@ def refresh_bot_data(self, bot_data: BD) -> None: bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``. """ raise NotImplementedError( - 'refresh_bot_data ist not implemented for this persistence class.' + 'refresh_bot_data is not implemented for this persistence class.' ) def flush(self) -> None: From 39a9f833104556047506541bf5bcb4873a1443cf Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 15 May 2021 15:51:48 +0200 Subject: [PATCH 22/31] Drop double copying from persistence & adjust tests --- telegram/ext/basepersistence.py | 4 + telegram/ext/dictpersistence.py | 9 +-- tests/test_persistence.py | 126 ++++++++++++++++++++++---------- 3 files changed, 94 insertions(+), 45 deletions(-) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index 3e83b6594f2..8646a541b00 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -83,6 +83,10 @@ class BasePersistence(Generic[UD, CD, BD], ABC): def __new__( cls, *args: object, **kwargs: object # pylint: disable=W0613 ) -> 'BasePersistence': + """This overrides the get_* and update_* methods to use insert/replace_bot. + That has the side effect that we always pass deepcopied data to those methods, so in + Pickle/DictPersistence we don't have to worry about copying the data again. + """ instance = super().__new__(cls) get_user_data = instance.get_user_data get_chat_data = instance.get_chat_data diff --git a/telegram/ext/dictpersistence.py b/telegram/ext/dictpersistence.py index 7224356a881..ea4c15a4846 100644 --- a/telegram/ext/dictpersistence.py +++ b/telegram/ext/dictpersistence.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the DictPersistence class.""" -from copy import deepcopy from typing import DefaultDict, Dict, Optional, Tuple from collections import defaultdict @@ -191,7 +190,7 @@ def get_user_data(self) -> DefaultDict[int, Dict[object, object]]: pass else: self._user_data = defaultdict(dict) - return deepcopy(self.user_data) # type: ignore[arg-type] + return self.user_data # type: ignore[return-value] def get_chat_data(self) -> DefaultDict[int, Dict[object, object]]: """Returns the chat_data created from the ``chat_data_json`` or an empty @@ -204,7 +203,7 @@ def get_chat_data(self) -> DefaultDict[int, Dict[object, object]]: pass else: self._chat_data = defaultdict(dict) - return deepcopy(self.chat_data) # type: ignore[arg-type] + return self.chat_data # type: ignore[return-value] def get_bot_data(self) -> Dict[object, object]: """Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`. @@ -216,7 +215,7 @@ def get_bot_data(self) -> Dict[object, object]: pass else: self._bot_data = {} - return deepcopy(self.bot_data) # type: ignore[arg-type] + return self.bot_data # type: ignore[return-value] def get_conversations(self, name: str) -> ConversationDict: """Returns the conversations created from the ``conversations_json`` or an empty @@ -284,7 +283,7 @@ def update_bot_data(self, data: Dict) -> None: """ if self._bot_data == data: return - self._bot_data = data.copy() + self._bot_data = data self._bot_data_json = None def refresh_user_data(self, user_id: int, user_data: Dict) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8b9f49a7e40..6e27978c118 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -133,12 +133,16 @@ def bot_data(): @pytest.fixture(scope="function") def chat_data(): - return defaultdict(dict, {-12345: {'test1': 'test2'}, -67890: {3: 'test4'}}) + return defaultdict( + dict, {-12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, -67890: {3: 'test4'}} + ) @pytest.fixture(scope="function") def user_data(): - return defaultdict(dict, {12345: {'test1': 'test2'}, 67890: {3: 'test4'}}) + return defaultdict( + dict, {12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, 67890: {3: 'test4'}} + ) @pytest.fixture(scope='function') @@ -958,25 +962,34 @@ def test_with_single_file_wo_bot_data(self, pickle_persistence, pickle_files_wo_ def test_updating_multi_file(self, pickle_persistence, good_pickle_files): user_data = pickle_persistence.get_user_data() - user_data[54321]['test9'] = 'test 10' + user_data[12345]['test3']['test4'] = 'test6' assert not pickle_persistence.user_data == user_data - pickle_persistence.update_user_data(54321, user_data[54321]) + pickle_persistence.update_user_data(12345, user_data[12345]) + user_data[12345]['test3']['test4'] = 'test7' + assert not pickle_persistence.user_data == user_data + pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data with open('pickletest_user_data', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)) assert user_data_test == user_data chat_data = pickle_persistence.get_chat_data() - chat_data[54321]['test9'] = 'test 10' + chat_data[-12345]['test3']['test4'] = 'test6' assert not pickle_persistence.chat_data == chat_data - pickle_persistence.update_chat_data(54321, chat_data[54321]) + pickle_persistence.update_chat_data(-12345, chat_data[-12345]) + chat_data[-12345]['test3']['test4'] = 'test7' + assert not pickle_persistence.chat_data == chat_data + pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data with open('pickletest_chat_data', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)) assert chat_data_test == chat_data bot_data = pickle_persistence.get_bot_data() - bot_data['test6'] = 'test 7' + bot_data['test3']['test4'] = 'test6' + assert not pickle_persistence.bot_data == bot_data + pickle_persistence.update_bot_data(bot_data) + bot_data['test3']['test4'] = 'test7' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data @@ -1003,25 +1016,34 @@ def test_updating_single_file(self, pickle_persistence, good_pickle_files): pickle_persistence.single_file = True user_data = pickle_persistence.get_user_data() - user_data[54321]['test9'] = 'test 10' + user_data[12345]['test3']['test4'] = 'test6' assert not pickle_persistence.user_data == user_data - pickle_persistence.update_user_data(54321, user_data[54321]) + pickle_persistence.update_user_data(12345, user_data[12345]) + user_data[12345]['test3']['test4'] = 'test7' + assert not pickle_persistence.user_data == user_data + pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data with open('pickletest', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)['user_data']) assert user_data_test == user_data chat_data = pickle_persistence.get_chat_data() - chat_data[54321]['test9'] = 'test 10' + chat_data[-12345]['test3']['test4'] = 'test6' assert not pickle_persistence.chat_data == chat_data - pickle_persistence.update_chat_data(54321, chat_data[54321]) + pickle_persistence.update_chat_data(-12345, chat_data[-12345]) + chat_data[-12345]['test3']['test4'] = 'test7' + assert not pickle_persistence.chat_data == chat_data + pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data with open('pickletest', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)['chat_data']) assert chat_data_test == chat_data bot_data = pickle_persistence.get_bot_data() - bot_data['test6'] = 'test 7' + bot_data['test3']['test4'] = 'test6' + assert not pickle_persistence.bot_data == bot_data + pickle_persistence.update_bot_data(bot_data) + bot_data['test3']['test4'] = 'test7' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data @@ -1571,7 +1593,7 @@ def test_json_outputs(self, user_data_json, chat_data_json, bot_data_json, conve assert dict_persistence.bot_data_json == bot_data_json assert dict_persistence.conversations_json == conversations_json - def test_json_changes( + def test_updating( self, user_data, user_data_json, @@ -1588,35 +1610,59 @@ def test_json_changes( bot_data_json=bot_data_json, conversations_json=conversations_json, ) - user_data_two = user_data.copy() - user_data_two.update({4: {5: 6}}) - dict_persistence.update_user_data(4, {5: 6}) - assert dict_persistence.user_data == user_data_two - assert dict_persistence.user_data_json != user_data_json - assert dict_persistence.user_data_json == json.dumps(user_data_two) - - chat_data_two = chat_data.copy() - chat_data_two.update({7: {8: 9}}) - dict_persistence.update_chat_data(7, {8: 9}) - assert dict_persistence.chat_data == chat_data_two - assert dict_persistence.chat_data_json != chat_data_json - assert dict_persistence.chat_data_json == json.dumps(chat_data_two) - - bot_data_two = bot_data.copy() - bot_data_two.update({'7': {'8': '9'}}) - bot_data['7'] = {'8': '9'} + + user_data = dict_persistence.get_user_data() + user_data[12345]['test3']['test4'] = 'test6' + assert not dict_persistence.user_data == user_data + assert not dict_persistence.user_data_json == json.dumps(user_data) + dict_persistence.update_user_data(12345, user_data[12345]) + user_data[12345]['test3']['test4'] = 'test7' + assert not dict_persistence.user_data == user_data + assert not dict_persistence.user_data_json == json.dumps(user_data) + dict_persistence.update_user_data(12345, user_data[12345]) + assert dict_persistence.user_data == user_data + assert dict_persistence.user_data_json == json.dumps(user_data) + + chat_data = dict_persistence.get_chat_data() + chat_data[-12345]['test3']['test4'] = 'test6' + assert not dict_persistence.chat_data == chat_data + assert not dict_persistence.chat_data_json == json.dumps(chat_data) + dict_persistence.update_chat_data(-12345, chat_data[-12345]) + chat_data[-12345]['test3']['test4'] = 'test7' + assert not dict_persistence.chat_data == chat_data + assert not dict_persistence.chat_data_json == json.dumps(chat_data) + dict_persistence.update_chat_data(-12345, chat_data[-12345]) + assert dict_persistence.chat_data == chat_data + assert dict_persistence.chat_data_json == json.dumps(chat_data) + + bot_data = dict_persistence.get_bot_data() + bot_data['test3']['test4'] = 'test6' + assert not dict_persistence.bot_data == bot_data + assert not dict_persistence.bot_data_json == json.dumps(bot_data) + dict_persistence.update_bot_data(bot_data) + bot_data['test3']['test4'] = 'test7' + assert not dict_persistence.bot_data == bot_data + assert not dict_persistence.bot_data_json == json.dumps(bot_data) dict_persistence.update_bot_data(bot_data) - assert dict_persistence.bot_data == bot_data_two - assert dict_persistence.bot_data_json != bot_data_json - assert dict_persistence.bot_data_json == json.dumps(bot_data_two) - - conversations_two = conversations.copy() - conversations_two.update({'name4': {(1, 2): 3}}) - dict_persistence.update_conversation('name4', (1, 2), 3) - assert dict_persistence.conversations == conversations_two - assert dict_persistence.conversations_json != conversations_json + assert dict_persistence.bot_data == bot_data + assert dict_persistence.bot_data_json == json.dumps(bot_data) + + conversation1 = dict_persistence.get_conversations('name1') + conversation1[(123, 123)] = 5 + assert not dict_persistence.conversations['name1'] == conversation1 + dict_persistence.update_conversation('name1', (123, 123), 5) + assert dict_persistence.conversations['name1'] == conversation1 + print(dict_persistence.conversations_json) + conversations['name1'][(123, 123)] = 5 + assert dict_persistence.conversations_json == encode_conversations_to_json(conversations) + assert dict_persistence.get_conversations('name1') == conversation1 + + dict_persistence._conversations = None + dict_persistence.update_conversation('name1', (123, 123), 5) + assert dict_persistence.conversations['name1'] == {(123, 123): 5} + assert dict_persistence.get_conversations('name1') == {(123, 123): 5} assert dict_persistence.conversations_json == encode_conversations_to_json( - conversations_two + {"name1": {(123, 123): 5}} ) def test_with_handler(self, bot, update): From c16a64792276b24a06ec96b22a18d4b0ec4ce179 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Thu, 27 May 2021 20:20:15 +0200 Subject: [PATCH 23/31] Add example to examples directory --- examples/README.md | 5 +- examples/contexttypesbot.py | 130 ++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 examples/contexttypesbot.py diff --git a/examples/README.md b/examples/README.md index 5b05c53ef5f..7d8f192256e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,7 +47,10 @@ A basic example of a bot that can accept payments. Don't forget to enable and co A basic example on how to set up a custom error handler. ### [`chatmemberbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/chatmemberbot.py) -A basic example on how `(my_)chat_member` updates can be used. +A basic example on how `(my_)chat_member` updates can be used. + +### [`contexttypesbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/contexttypesbot.py) +This example showcases how `telegram.ext.ContextTypes` can be used to customize the `context` argument of handler and job callbacks. ## Pure API The [`rawapibot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/rawapibot.py) example uses only the pure, "bare-metal" API wrapper. diff --git a/examples/contexttypesbot.py b/examples/contexttypesbot.py new file mode 100644 index 00000000000..9f53656a90a --- /dev/null +++ b/examples/contexttypesbot.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# pylint: disable=C0116 +# This program is dedicated to the public domain under the CC0 license. + +""" +Simple Bot to showcase `telegram.ext.ContextTypes`. + +Usage: +Press Ctrl-C on the command line or send a signal to the process to stop the +bot. +""" + +from collections import defaultdict +from typing import DefaultDict, Optional, Set + +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode +from telegram.ext import ( + Updater, + CommandHandler, + CallbackContext, + ContextTypes, + CallbackQueryHandler, + TypeHandler, + Dispatcher, +) + + +class ChatData: + """Custom class for chat_data. Here we store data per message.""" + + def __init__(self) -> None: + self.clicks_per_message: DefaultDict[int, int] = defaultdict(int) + + +# The [dict, ChatData, dict] is for type checkers like mypy +class CustomContext(CallbackContext[dict, ChatData, dict]): + """Custom class for context.""" + + def __init__(self, dispatcher: Dispatcher): + super().__init__(dispatcher=dispatcher) + self._message_id: Optional[int] = None + + @property + def bot_user_ids(self) -> Set[int]: + """Custom shortcut to access a value stored in the bot_data dict""" + return self.bot_data.setdefault('user_ids', set()) + + @property + def message_clicks(self) -> Optional[int]: + """Access the number of clicks for the message this context object was built for.""" + if self._message_id: + return self.chat_data.clicks_per_message[self._message_id] + return None + + @message_clicks.setter + def message_clicks(self, value: int) -> None: + """Allow to change the count""" + if not self._message_id: + raise RuntimeError('There is no message associated with this context obejct.') + self.chat_data.clicks_per_message[self._message_id] = value + + @classmethod + def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CustomContext': + """Override from_update to set _message_id.""" + # Make sure to call super() + context = super().from_update(update, dispatcher) + + if context.chat_data: + if isinstance(update, Update) and update.effective_message: + context._message_id = update.effective_message.message_id # pylint: disable=W0212 + + # Remember to return the object + return context + + +def start(update: Update, _: CustomContext) -> None: + """Display a message with a button.""" + update.message.reply_html( + 'This button was clicked 0 times.', + reply_markup=InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text='Click me!', callback_data='button') + ), + ) + + +def count_click(update: Update, context: CustomContext) -> None: + """Update the click count for the message.""" + context.message_clicks += 1 + update.callback_query.answer() + update.effective_message.edit_text( + f'This button was clicked {context.message_clicks} times.', + reply_markup=InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text='Click me!', callback_data='button') + ), + parse_mode=ParseMode.HTML, + ) + + +def print_users(update: Update, context: CustomContext) -> None: + """Show which users have been using this bot.""" + update.message.reply_text( + 'The following user IDs have used this bot: ' + f'{", ".join(map(str, context.bot_user_ids))}' + ) + + +def track_users(update: Update, context: CustomContext) -> None: + """Store the user id of the incoming update, if any.""" + if update.effective_user: + context.bot_user_ids.add(update.effective_user.id) + + +def main() -> None: + """Run the bot.""" + context_types = ContextTypes(context=CustomContext, chat_data=ChatData) + updater = Updater("TOKEN", context_types=context_types) + + dispatcher = updater.dispatcher + # run track_users in its own group to not interfere with the user handlers + dispatcher.add_handler(TypeHandler(Update, track_users), group=-1) + dispatcher.add_handler(CommandHandler("start", start)) + dispatcher.add_handler(CallbackQueryHandler(count_click)) + dispatcher.add_handler(CommandHandler("print_users", print_users)) + + updater.start_polling() + updater.idle() + + +if __name__ == '__main__': + main() From cb17b8537509b86793fcb0ae590171b34c945923 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Thu, 27 May 2021 20:25:09 +0200 Subject: [PATCH 24/31] Deepsource --- examples/contexttypesbot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/contexttypesbot.py b/examples/contexttypesbot.py index 9f53656a90a..9f52568cdfb 100644 --- a/examples/contexttypesbot.py +++ b/examples/contexttypesbot.py @@ -65,9 +65,8 @@ def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CustomContext # Make sure to call super() context = super().from_update(update, dispatcher) - if context.chat_data: - if isinstance(update, Update) and update.effective_message: - context._message_id = update.effective_message.message_id # pylint: disable=W0212 + if context.chat_data and isinstance(update, Update) and update.effective_message: + context._message_id = update.effective_message.message_id # pylint: disable=W0212 # Remember to return the object return context From e4a05d9e3dc0eca72cb5c298b8c0331556c72f4b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 28 May 2021 20:55:05 +0200 Subject: [PATCH 25/31] review --- examples/contexttypesbot.py | 6 +++--- telegram/ext/picklepersistence.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/contexttypesbot.py b/examples/contexttypesbot.py index 9f52568cdfb..cfe485a61f8 100644 --- a/examples/contexttypesbot.py +++ b/examples/contexttypesbot.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# pylint: disable=C0116 +# pylint: disable=C0116,W0613 # This program is dedicated to the public domain under the CC0 license. """ @@ -72,7 +72,7 @@ def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CustomContext return context -def start(update: Update, _: CustomContext) -> None: +def start(update: Update, context: CustomContext) -> None: """Display a message with a button.""" update.message.reply_html( 'This button was clicked 0 times.', @@ -83,7 +83,7 @@ def start(update: Update, _: CustomContext) -> None: def count_click(update: Update, context: CustomContext) -> None: - """Update the click count for the message.""" + """Update the click count for the message.""" context.message_clicks += 1 update.callback_query.answer() update.effective_message.edit_text( diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index 6f5831285aa..c5eaa2d0fbd 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -233,7 +233,7 @@ def get_bot_data(self) -> BD: :class:`telegram.ext.utils.types.BD`. Returns: - :class:`telegram.ext.utils.types.BD`: The restored user data. + :class:`telegram.ext.utils.types.BD`: The restored bot data. """ if self.bot_data: pass From 45eae2088e1caaff4d60a2949ef76ed3f06a3c06 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Fri, 28 May 2021 22:29:15 +0200 Subject: [PATCH 26/31] Make BP.refresh_* methods non-breaking --- telegram/ext/basepersistence.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index b867fe86d0c..38d75aadbc2 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -416,9 +416,6 @@ def refresh_user_data(self, user_id: int, user_data: UD) -> None: user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with. user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user. """ - raise NotImplementedError( - 'refresh_user_data is not implemented for this persistence class.' - ) def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` before passing the @@ -431,9 +428,6 @@ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with. chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat. """ - raise NotImplementedError( - 'refresh_chat_data is not implemented for this persistence class.' - ) def refresh_bot_data(self, bot_data: BD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` before passing the @@ -445,9 +439,6 @@ def refresh_bot_data(self, bot_data: BD) -> None: Args: bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``. """ - raise NotImplementedError( - 'refresh_bot_data is not implemented for this persistence class.' - ) def flush(self) -> None: """Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the From ae7b6e5fc9b82617b1c0dedceae371bfcdfa91a3 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 30 May 2021 18:52:14 +0200 Subject: [PATCH 27/31] Fix slots for Updater on py3.6 --- telegram/ext/updater.py | 57 ++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index a110f051168..fe04356291b 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -21,6 +21,7 @@ import logging import ssl import warnings +from sys import version_info as py_ver from queue import Queue from signal import SIGABRT, SIGINT, SIGTERM, signal from threading import Event, Lock, Thread, current_thread @@ -124,24 +125,44 @@ class Updater(Generic[CCT, UD, CD, BD]): """ - __slots__ = ( - 'persistence', - 'dispatcher', - 'user_sig_handler', - 'bot', - 'logger', - 'update_queue', - 'job_queue', - '__exception_event', - 'last_update_id', - 'running', - '_request', - 'is_idle', - 'httpd', - '__lock', - '__threads', - '__dict__', - ) + # Apparently Py 3.7 and below have '__dict__' in ABC and Generic apparently is abstract + if py_ver < (3, 7): + __slots__ = ( + 'persistence', + 'dispatcher', + 'user_sig_handler', + 'bot', + 'logger', + 'update_queue', + 'job_queue', + '__exception_event', + 'last_update_id', + 'running', + '_request', + 'is_idle', + 'httpd', + '__lock', + '__threads', + ) + else: + __slots__ = ( + 'persistence', # type: ignore[assignment] + 'dispatcher', + 'user_sig_handler', + 'bot', + 'logger', + 'update_queue', + 'job_queue', + '__exception_event', + 'last_update_id', + 'running', + '_request', + 'is_idle', + 'httpd', + '__lock', + '__threads', + '__dict__', + ) @overload def __init__( From 2ca7f174c7d07cb18bd5f5baf8bc36b9bc76446b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 30 May 2021 19:42:02 +0200 Subject: [PATCH 28/31] Fix slot tests --- tests/test_slots.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_slots.py b/tests/test_slots.py index eb37db6b59e..e97a4e17835 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.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 os import importlib import importlib.util from glob import iglob @@ -34,11 +35,15 @@ def test_class_has_slots_and_dict(mro_slots): - tg_paths = [p for p in iglob("../telegram/**/*.py", recursive=True) if '/vendor/' not in p] + tg_paths = [p for p in iglob("telegram/**/*.py", recursive=True) if 'vendor' not in p] for path in tg_paths: - split_path = path.split('/') - mod_name = f"telegram{'.ext.' if split_path[2] == 'ext' else '.'}{split_path[-1][:-3]}" + # windows uses backslashes: + if os.name == 'nt': + split_path = path.split('\\') + else: + split_path = path.split('/') + mod_name = f"telegram{'.ext.' if split_path[1] == 'ext' else '.'}{split_path[-1][:-3]}" spec = importlib.util.spec_from_file_location(mod_name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # Exec module to get classes in it. @@ -55,4 +60,20 @@ def test_class_has_slots_and_dict(mro_slots): def get_slots(_class): - return [attr for cls in _class.__mro__ if hasattr(cls, '__slots__') for attr in cls.__slots__] + slots = [attr for cls in _class.__mro__ if hasattr(cls, '__slots__') for attr in cls.__slots__] + + # We're a bit hacky here to handle cases correctly, where we can't read the parents slots from + # the mro + if '__dict__' not in slots: + try: + + class Subclass(_class): + __slots__ = ('__dict__',) + + except TypeError as exc: + if '__dict__ slot disallowed: we already got one' in str(exc): + slots.append('__dict__') + else: + raise exc + + return slots From 23c457777a98100f9fd0a474fa5613c5560b7a5d Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 30 May 2021 21:11:18 +0200 Subject: [PATCH 29/31] =?UTF-8?q?More=20slot=20fixes=20=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- telegram/ext/__init__.py | 13 +++++++++ telegram/ext/updater.py | 57 +++++++++++++--------------------------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 6854f7114c3..8b2b772fcbe 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.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/]. +# pylint: disable=C0413 """Extensions over the Telegram Bot API to facilitate bot making""" from .basepersistence import BasePersistence @@ -25,6 +26,18 @@ from .callbackcontext import CallbackContext from .contexttypes import ContextTypes from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async + +# https://bugs.python.org/issue41451, fixed on 3.7+, doesn't actually remove slots +# try-except is just here in case the __init__ is called twice (like in the tests) +# this block is also the reason for the pylint-ignore at the top of the file +try: + del Dispatcher.__slots__ +except AttributeError as exc: + if str(exc) == '__slots__': + pass + else: + raise exc + from .jobqueue import JobQueue, Job from .updater import Updater from .callbackqueryhandler import CallbackQueryHandler diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index fe04356291b..a110f051168 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -21,7 +21,6 @@ import logging import ssl import warnings -from sys import version_info as py_ver from queue import Queue from signal import SIGABRT, SIGINT, SIGTERM, signal from threading import Event, Lock, Thread, current_thread @@ -125,44 +124,24 @@ class Updater(Generic[CCT, UD, CD, BD]): """ - # Apparently Py 3.7 and below have '__dict__' in ABC and Generic apparently is abstract - if py_ver < (3, 7): - __slots__ = ( - 'persistence', - 'dispatcher', - 'user_sig_handler', - 'bot', - 'logger', - 'update_queue', - 'job_queue', - '__exception_event', - 'last_update_id', - 'running', - '_request', - 'is_idle', - 'httpd', - '__lock', - '__threads', - ) - else: - __slots__ = ( - 'persistence', # type: ignore[assignment] - 'dispatcher', - 'user_sig_handler', - 'bot', - 'logger', - 'update_queue', - 'job_queue', - '__exception_event', - 'last_update_id', - 'running', - '_request', - 'is_idle', - 'httpd', - '__lock', - '__threads', - '__dict__', - ) + __slots__ = ( + 'persistence', + 'dispatcher', + 'user_sig_handler', + 'bot', + 'logger', + 'update_queue', + 'job_queue', + '__exception_event', + 'last_update_id', + 'running', + '_request', + 'is_idle', + 'httpd', + '__lock', + '__threads', + '__dict__', + ) @overload def __init__( From 871f33470910cb842fc1f1998fd32214e18fcfd4 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sun, 30 May 2021 21:22:13 +0200 Subject: [PATCH 30/31] Try fixing pre-commit --- telegram/ext/__init__.py | 2 +- tests/test_slots.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 8b2b772fcbe..93f5615144a 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -31,7 +31,7 @@ # try-except is just here in case the __init__ is called twice (like in the tests) # this block is also the reason for the pylint-ignore at the top of the file try: - del Dispatcher.__slots__ + del Dispatcher.__slots__ # type: ignore[has-type] except AttributeError as exc: if str(exc) == '__slots__': pass diff --git a/tests/test_slots.py b/tests/test_slots.py index 3e3ed4e8454..9d5169eb392 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -19,7 +19,6 @@ import os import importlib import importlib.util -import os from glob import iglob import inspect From d6b04eafdc1d46333d39719ff4417d11fb7ff98b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Thu, 3 Jun 2021 21:00:43 +0200 Subject: [PATCH 31/31] Some more versioning directives --- telegram/ext/callbackcontext.py | 9 +++++---- telegram/ext/dictpersistence.py | 3 +++ telegram/ext/picklepersistence.py | 3 +++ telegram/ext/utils/types.py | 25 ++++++++++++++++++++----- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index 5216b43e474..626af5f83e3 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -178,12 +178,13 @@ def user_data(self, value: object) -> NoReturn: ) def refresh_data(self) -> None: - """ - If :attr:`dispatcher` uses persistence, calls + """If :attr:`dispatcher` uses persistence, calls :meth:`telegram.ext.BasePersistence.refresh_bot_data` on :attr:`bot_data`, - :meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data`, + :meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data` and :meth:`telegram.ext.BasePersistence.refresh_user_data` on :attr:`user_data`, if appropriate. + + .. versionadded:: 13.6 """ if self.dispatcher.persistence: if self.dispatcher.persistence.store_bot_data: @@ -239,7 +240,7 @@ def from_update(cls: Type[CC], update: object, dispatcher: 'Dispatcher') -> CC: .. seealso:: :meth:`telegram.ext.Dispatcher.add_handler` Args: - update (:obj:`any` | :class:`telegram.Update`): The update. + update (:obj:`object` | :class:`telegram.Update`): The update. dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context. diff --git a/telegram/ext/dictpersistence.py b/telegram/ext/dictpersistence.py index e9c0982890d..ad936044292 100644 --- a/telegram/ext/dictpersistence.py +++ b/telegram/ext/dictpersistence.py @@ -300,17 +300,20 @@ def update_bot_data(self, data: Dict) -> None: def refresh_user_data(self, user_id: int, user_data: Dict) -> None: """Does nothing. + .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` """ def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None: """Does nothing. + .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` """ def refresh_bot_data(self, bot_data: Dict) -> None: """Does nothing. + .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` """ diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index b86b0f61192..d015924b7e3 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -362,18 +362,21 @@ def update_bot_data(self, data: BD) -> None: def refresh_user_data(self, user_id: int, user_data: UD) -> None: """Does nothing. + .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` """ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: """Does nothing. + .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` """ def refresh_bot_data(self, bot_data: BD) -> None: """Does nothing. + .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` """ diff --git a/telegram/ext/utils/types.py b/telegram/ext/utils/types.py index 4769d1fe28a..fbaedd1652c 100644 --- a/telegram/ext/utils/types.py +++ b/telegram/ext/utils/types.py @@ -16,17 +16,32 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -"""This module contains custom typing aliases.""" +"""This module contains custom typing aliases. + +.. versionadded:: 13.6 +""" from typing import TypeVar, TYPE_CHECKING if TYPE_CHECKING: from telegram.ext import CallbackContext # noqa: F401 CCT = TypeVar('CCT', bound='CallbackContext') -"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.""" +"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass. + +.. versionadded:: 13.6 +""" UD = TypeVar('UD') -"""Type of the user data for a single user.""" +"""Type of the user data for a single user. + +.. versionadded:: 13.6 +""" CD = TypeVar('CD') -"""Type of the chat data for a single user.""" +"""Type of the chat data for a single user. + +.. versionadded:: 13.6 +""" BD = TypeVar('BD') -"""Type of the bot data.""" +"""Type of the bot data. + +.. versionadded:: 13.6 +"""