diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index c36504deb..453845ff4 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -68,7 +68,9 @@ Here's how to make a one-off code change. - You can refer to relevant issues in the commit message by writing, e.g., "#105". - Your code should adhere to the `PEP 8 Style Guide`_, with the exception that we have a maximum line length of 99. - + + - Provide static typing with signature annotations. The documentation of `MyPy`_ will be a good start, the cheat sheet is `here`_. We also have some custom type aliases in ``telegram.utils.helpers.typing``. + - Document your code. This project uses `sphinx`_ to generate static HTML docs. To build them, first make sure you have the required dependencies: .. code-block:: bash @@ -251,3 +253,5 @@ break the API classes. For example: .. _`Google Python Style Guide`: http://google.github.io/styleguide/pyguide.html .. _`Google Python Style Docstrings`: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html .. _AUTHORS.rst: ../AUTHORS.rst +.. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html +.. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html diff --git a/.gitignore b/.gitignore index a98e967bc..a2e9366dd 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ htmlcov/ .coverage.* .cache .pytest_cache +.mypy_cache nosetests.xml coverage.xml *,cover diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8d0a0a5a..dff4bf4e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: args: - --diff - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.1 + rev: 3.8.1 hooks: - id: flake8 - repo: git://github.com/pre-commit/mirrors-pylint @@ -18,3 +18,8 @@ repos: args: - --errors-only - --disable=import-error +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.770' + hooks: + - id: mypy + files: ^telegram/.*\.py$ diff --git a/Makefile b/Makefile index ac90c183a..3060dbc80 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ PYTEST := pytest PEP257 := pep257 PEP8 := flake8 YAPF := yapf +MYPY := mypy PIP := pip clean: @@ -28,6 +29,9 @@ yapf: lint: $(PYLINT) -E telegram --disable=no-name-in-module,import-error +mypy: + $(MYPY) -p telegram + test: $(PYTEST) -v @@ -41,6 +45,7 @@ help: @echo "- pep8 Check style with flake8" @echo "- lint Check style with pylint" @echo "- yapf Check style with yapf" + @echo "- mypy Check type hinting with mypy" @echo "- test Run tests using pytest" @echo @echo "Available variables:" @@ -49,4 +54,5 @@ help: @echo "- PEP257 default: $(PEP257)" @echo "- PEP8 default: $(PEP8)" @echo "- YAPF default: $(YAPF)" + @echo "- MYPY default: $(MYPY)" @echo "- PIP default: $(PIP)" diff --git a/docs/source/telegram.utils.rst b/docs/source/telegram.utils.rst index a80347237..619918b1a 100644 --- a/docs/source/telegram.utils.rst +++ b/docs/source/telegram.utils.rst @@ -6,3 +6,4 @@ telegram.utils package telegram.utils.helpers telegram.utils.promise telegram.utils.request + telegram.utils.types diff --git a/docs/source/telegram.utils.types.rst b/docs/source/telegram.utils.types.rst new file mode 100644 index 000000000..fd1c0252b --- /dev/null +++ b/docs/source/telegram.utils.types.rst @@ -0,0 +1,6 @@ +telegram.utils.types Module +=========================== + +.. automodule:: telegram.utils.types + :members: + :show-inheritance: diff --git a/requirements-dev.txt b/requirements-dev.txt index 577e6dd53..be7c179c6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,7 @@ pep257 pylint flaky yapf +mypy==0.770 pre-commit beautifulsoup4 pytest==4.2.0 diff --git a/setup.cfg b/setup.cfg index e30e2fdac..6cb212922 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,3 +40,22 @@ omit = telegram/__main__.py telegram/vendor/* +[coverage:report] +exclude_lines = + if TYPE_CHECKING: + +[mypy] +warn_unused_ignores = True +warn_unused_configs = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +disallow_untyped_decorators = True +show_error_codes = True + +[mypy-telegram.vendor.*] +ignore_errors = True + +# Disable strict optional for telegram objects with class methods +# We don't want to clutter the code with 'if self.bot is None: raise RuntimeError()' +[mypy-telegram.callbackquery,telegram.chat,telegram.message,telegram.user,telegram.files.*,telegram.inline.inlinequery,telegram.payment.precheckoutquery,telegram.payment.shippingquery,telegram.passport.passportdata,telegram.passport.credentials,telegram.passport.passportfile,telegram.ext.filters] +strict_optional = False diff --git a/telegram/__init__.py b/telegram/__init__.py index 50ea1027e..1e493ab8b 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -104,7 +104,6 @@ from .games.gamehighscore import GameHighScore from .update import Update from .files.inputmedia import (InputMedia, InputMediaVideo, InputMediaPhoto, InputMediaAnimation, InputMediaAudio, InputMediaDocument) -from .bot import Bot from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS, MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD, MAX_MESSAGES_PER_SECOND_PER_CHAT, MAX_MESSAGES_PER_SECOND, @@ -124,6 +123,7 @@ from .passport.credentials import (Credentials, SecureData, FileCredentials, TelegramDecryptionError) +from .bot import Bot from .version import __version__ # noqa: F401 __author__ = 'devs@python-telegram-bot.org' diff --git a/telegram/__main__.py b/telegram/__main__.py index d314679ae..831aaa046 100644 --- a/telegram/__main__.py +++ b/telegram/__main__.py @@ -21,11 +21,12 @@ import subprocess import certifi +from typing import Optional from . import __version__ as telegram_ver -def _git_revision(): +def _git_revision() -> Optional[str]: try: output = subprocess.check_output(["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT) @@ -34,15 +35,15 @@ def _git_revision(): return output.decode().strip() -def print_ver_info(): +def print_ver_info() -> None: git_revision = _git_revision() print('python-telegram-bot {}'.format(telegram_ver) + (' ({})'.format(git_revision) if git_revision else '')) - print('certifi {}'.format(certifi.__version__)) + print('certifi {}'.format(certifi.__version__)) # type: ignore[attr-defined] print('Python {}'.format(sys.version.replace('\n', ' '))) -def main(): +def main() -> None: print_ver_info() diff --git a/telegram/base.py b/telegram/base.py index d93233002..558793918 100644 --- a/telegram/base.py +++ b/telegram/base.py @@ -17,36 +17,64 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """Base class for Telegram Objects.""" - try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] import warnings +from telegram.utils.types import JSONDict +from typing import Tuple, Any, Optional, Type, TypeVar, TYPE_CHECKING, List + +if TYPE_CHECKING: + from telegram import Bot + +TO = TypeVar('TO', bound='TelegramObject', covariant=True) + class TelegramObject: """Base class for most telegram objects.""" - _id_attrs = () + # def __init__(self, *args: Any, **kwargs: Any): + # pass - def __str__(self): + _id_attrs: Tuple[Any, ...] = () + + def __str__(self) -> str: return str(self.to_dict()) - def __getitem__(self, item): + def __getitem__(self, item: str) -> Any: return self.__dict__[item] + @staticmethod + def parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]: + if not data: + return None + return data.copy() + @classmethod - def de_json(cls, data, bot): + def de_json(cls: Type[TO], data: Optional[JSONDict], bot: 'Bot') -> Optional[TO]: + data = cls.parse_data(data) + if not data: return None - data = data.copy() + if cls == TelegramObject: + return cls() + else: + return cls(bot=bot, **data) # type: ignore[call-arg] - return data + @classmethod + def de_list(cls: Type[TO], + data: Optional[List[JSONDict]], + bot: 'Bot') -> List[Optional[TO]]: + if not data: + return [] - def to_json(self): + return [cls.de_json(d, bot) for d in data] + + def to_json(self) -> str: """ Returns: :obj:`str` @@ -55,7 +83,7 @@ class TelegramObject: return json.dumps(self.to_dict()) - def to_dict(self): + def to_dict(self) -> JSONDict: data = dict() for key in iter(self.__dict__): @@ -73,7 +101,7 @@ class TelegramObject: data['from'] = data.pop('from_user', None) return data - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): if self._id_attrs == (): warnings.warn("Objects of type {} can not be meaningfully tested for " @@ -84,7 +112,7 @@ class TelegramObject: return self._id_attrs == other._id_attrs return super().__eq__(other) # pylint: disable=no-member - def __hash__(self): + def __hash__(self) -> int: if self._id_attrs: return hash((self.__class__, self._id_attrs)) # pylint: disable=no-member return super().__hash__() diff --git a/telegram/bot.py b/telegram/bot.py index 54a251458..b5f588500 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -22,12 +22,13 @@ import functools import inspect + from decorator import decorate try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] # noqa: F723 import logging from datetime import datetime @@ -37,16 +38,26 @@ from cryptography.hazmat.primitives import serialization from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File, ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet, PhotoSize, Audio, Document, Sticker, Video, Animation, Voice, VideoNote, - Location, Venue, Contact, InputFile, Poll, BotCommand) + Location, Venue, Contact, InputFile, Poll, BotCommand, ChatAction, + InlineQueryResult, InputMedia, PassportElementError, MaskPosition, + ChatPermissions, ShippingOption, LabeledPrice, ChatPhoto) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError -from telegram.utils.helpers import to_timestamp, DEFAULT_NONE +from telegram.utils.helpers import to_timestamp, DEFAULT_NONE, DefaultValue from telegram.utils.request import Request +from telegram.utils.types import JSONDict, FileLike + +from typing import (Any, Callable, Optional, TypeVar, Union, TYPE_CHECKING, List, Tuple, + no_type_check, IO, cast) +if TYPE_CHECKING: + from telegram.ext import Defaults + +RT = TypeVar('RT') -def info(func): +def info(func: Callable[..., RT]) -> Callable[..., RT]: @functools.wraps(func) - def decorator(self, *args, **kwargs): + def decorator(self: 'Bot', *args: Any, **kwargs: Any) -> RT: if not self.bot: self.get_me() @@ -59,10 +70,10 @@ def info(func): return decorator -def log(func, *args, **kwargs): +def log(func: Callable[..., RT], *args: Any, **kwargs: Any) -> Callable[..., RT]: logger = logging.getLogger(func.__module__) - def decorator(self, *args, **kwargs): + def decorator(self: 'Bot', *args: Any, **kwargs: Any) -> RT: logger.debug('Entering: %s', func.__name__) result = func(*args, **kwargs) logger.debug(result) @@ -94,7 +105,7 @@ class Bot(TelegramObject): """ - def __new__(cls, *args, **kwargs): + def __new__(cls, *args: Any, **kwargs: Any) -> 'Bot': # Get default values from kwargs defaults = kwargs.get('defaults') @@ -107,7 +118,7 @@ class Bot(TelegramObject): # For each method ... for method_name, method in inspect.getmembers(instance, predicate=inspect.ismethod): # ... get kwargs - argspec = inspect.getargspec(method) + argspec = inspect.getfullargspec(method) kwarg_names = argspec.args[-len(argspec.defaults or []):] # ... check if Defaults has a attribute that matches the kwarg name needs_default = [ @@ -126,13 +137,13 @@ class Bot(TelegramObject): return instance def __init__(self, - token, - base_url=None, - base_file_url=None, - request=None, - private_key=None, - private_key_password=None, - defaults=None): + token: str, + base_url: str = None, + base_file_url: str = None, + request: 'Request' = None, + private_key: bytes = None, + private_key_password: bytes = None, + defaults: 'Defaults' = None): self.token = self._validate_token(token) # Gather default @@ -146,8 +157,8 @@ class Bot(TelegramObject): self.base_url = str(base_url) + str(self.token) self.base_file_url = str(base_file_url) + str(self.token) - self.bot = None - self._commands = None + self.bot: Optional[User] = None + self._commands: Optional[List[BotCommand]] = None self._request = request or Request() self.logger = logging.getLogger(__name__) @@ -156,7 +167,14 @@ class Bot(TelegramObject): password=private_key_password, backend=default_backend()) - def _post(self, endpoint, data=None, timeout=None, api_kwargs=None): + def _post(self, + endpoint: str, + data: JSONDict = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[bool, JSONDict, None]: + if data is None: + data = {} + if api_kwargs: if data: data.update(api_kwargs) @@ -166,8 +184,14 @@ class Bot(TelegramObject): return self.request.post('{}/{}'.format(self.base_url, endpoint), data=data, timeout=timeout) - def _message(self, endpoint, data, reply_to_message_id=None, disable_notification=None, - reply_markup=None, timeout=None, api_kwargs=None): + def _message(self, + endpoint: str, + data: JSONDict, + reply_to_message_id: Union[str, int] = None, + disable_notification: bool = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[bool, Message, None]: if reply_to_message_id is not None: data['reply_to_message_id'] = reply_to_message_id @@ -191,16 +215,16 @@ class Bot(TelegramObject): result = self._post(endpoint, data, timeout=timeout, api_kwargs=api_kwargs) if result is True: - return result + return result # type: ignore - return Message.de_json(result, self) + return Message.de_json(result, self) # type: ignore[arg-type] @property - def request(self): + def request(self) -> Request: return self._request @staticmethod - def _validate_token(token): + def _validate_token(token: str) -> str: """A very basic validation on token.""" if any(x.isspace() for x in token): raise InvalidToken() @@ -211,77 +235,77 @@ class Bot(TelegramObject): return token - @property + @property # type: ignore @info - def id(self): + def id(self) -> int: """:obj:`int`: Unique identifier for this bot.""" - return self.bot.id + return self.bot.id # type: ignore - @property + @property # type: ignore @info - def first_name(self): + def first_name(self) -> str: """:obj:`str`: Bot's first name.""" - return self.bot.first_name + return self.bot.first_name # type: ignore - @property + @property # type: ignore @info - def last_name(self): + def last_name(self) -> str: """:obj:`str`: Optional. Bot's last name.""" - return self.bot.last_name + return self.bot.last_name # type: ignore - @property + @property # type: ignore @info - def username(self): + def username(self) -> str: """:obj:`str`: Bot's username.""" - return self.bot.username + return self.bot.username # type: ignore - @property + @property # type: ignore @info - def link(self): + def link(self) -> str: """:obj:`str`: Convenience property. Returns the t.me link of the bot.""" return "https://t.me/{}".format(self.username) - @property + @property # type: ignore @info - def can_join_groups(self): - """:obj:`str`: Bot's can_join_groups attribute.""" + def can_join_groups(self) -> bool: + """:obj:`bool`: Bot's can_join_groups attribute.""" - return self.bot.can_join_groups + return self.bot.can_join_groups # type: ignore - @property + @property # type: ignore @info - def can_read_all_group_messages(self): - """:obj:`str`: Bot's can_read_all_group_messages attribute.""" + def can_read_all_group_messages(self) -> bool: + """:obj:`bool`: Bot's can_read_all_group_messages attribute.""" - return self.bot.can_read_all_group_messages + return self.bot.can_read_all_group_messages # type: ignore - @property + @property # type: ignore @info - def supports_inline_queries(self): - """:obj:`str`: Bot's supports_inline_queries attribute.""" + def supports_inline_queries(self) -> bool: + """:obj:`bool`: Bot's supports_inline_queries attribute.""" - return self.bot.supports_inline_queries + return self.bot.supports_inline_queries # type: ignore - @property + @property # type: ignore @info - def commands(self): + def commands(self) -> List[BotCommand]: """List[:class:`BotCommand`]: Bot's commands.""" - return self._commands + return self._commands or [] @property - def name(self): + def name(self) -> str: """:obj:`str`: Bot's @username.""" return '@{}'.format(self.username) @log - def get_me(self, timeout=None, api_kwargs=None): + def get_me(self, timeout: int = None, api_kwargs: JSONDict = None) -> Optional[User]: """A simple method for testing your bot's auth token. Requires no parameters. Args: @@ -301,21 +325,21 @@ class Bot(TelegramObject): """ result = self._post('getMe', timeout=timeout, api_kwargs=api_kwargs) - self.bot = User.de_json(result, self) + self.bot = User.de_json(result, self) # type: ignore return self.bot @log def send_message(self, - chat_id, - text, - parse_mode=None, - disable_web_page_preview=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - api_kwargs=None): + chat_id: Union[int, str], + text: str, + parse_mode: str = None, + disable_web_page_preview: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """Use this method to send text messages. Args: @@ -348,19 +372,24 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'text': text} + data: JSONDict = {'chat_id': chat_id, 'text': text} if parse_mode: data['parse_mode'] = parse_mode if disable_web_page_preview: data['disable_web_page_preview'] = disable_web_page_preview - return self._message('sendMessage', data, disable_notification=disable_notification, + return self._message('sendMessage', data, # type: ignore[return-value] + disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs) @log - def delete_message(self, chat_id, message_id, timeout=None, api_kwargs=None): + def delete_message(self, + chat_id: Union[str, int], + message_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to delete a message, including service messages, with the following limitations: @@ -392,20 +421,20 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'message_id': message_id} + data: JSONDict = {'chat_id': chat_id, 'message_id': message_id} result = self._post('deleteMessage', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log def forward_message(self, - chat_id, - from_chat_id, - message_id, - disable_notification=False, - timeout=None, - api_kwargs=None): + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_id: Union[str, int], + disable_notification: bool = False, + timeout: float = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """Use this method to forward messages of any kind. Args: @@ -429,7 +458,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {} + data: JSONDict = {} if chat_id: data['chat_id'] = chat_id @@ -438,20 +467,21 @@ class Bot(TelegramObject): if message_id: data['message_id'] = message_id - return self._message('forwardMessage', data, disable_notification=disable_notification, + return self._message('forwardMessage', data, # type: ignore[return-value] + disable_notification=disable_notification, timeout=timeout, api_kwargs=api_kwargs) @log def send_photo(self, - chat_id, - photo, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - api_kwargs=None): + chat_id: int, + photo: Union[str, PhotoSize, IO], + caption: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + parse_mode: str = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """Use this method to send photos. Note: @@ -492,35 +522,37 @@ class Bot(TelegramObject): if isinstance(photo, PhotoSize): photo = photo.file_id elif InputFile.is_file(photo): - photo = InputFile(photo) + photo = cast(IO, photo) + photo = InputFile(photo) # type: ignore[assignment] - data = {'chat_id': chat_id, 'photo': photo} + data: JSONDict = {'chat_id': chat_id, 'photo': photo} if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode - return self._message('sendPhoto', data, timeout=timeout, + return self._message('sendPhoto', data, # type: ignore[return-value] + timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_audio(self, - chat_id, - audio, - duration=None, - performer=None, - title=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - thumb=None, - api_kwargs=None): + chat_id: Union[int, str], + audio: Union[str, Audio, FileLike], + duration: int = None, + performer: str = None, + title: str = None, + caption: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + parse_mode: str = None, + thumb: FileLike = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .mp3 or .m4a format. @@ -576,9 +608,10 @@ class Bot(TelegramObject): if isinstance(audio, Audio): audio = audio.file_id elif InputFile.is_file(audio): + audio = cast(IO, audio) audio = InputFile(audio) - data = {'chat_id': chat_id, 'audio': audio} + data: JSONDict = {'chat_id': chat_id, 'audio': audio} if duration: data['duration'] = duration @@ -592,27 +625,29 @@ class Bot(TelegramObject): data['parse_mode'] = parse_mode if thumb: if InputFile.is_file(thumb): + thumb = cast(IO, thumb) thumb = InputFile(thumb, attach=True) data['thumb'] = thumb - return self._message('sendAudio', data, timeout=timeout, + return self._message('sendAudio', data, # type: ignore[return-value] + timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_document(self, - chat_id, - document, - filename=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - thumb=None, - api_kwargs=None): + chat_id: Union[int, str], + document: Union[str, Document, FileLike], + filename: str = None, + caption: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + parse_mode: str = None, + thumb: FileLike = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """ Use this method to send general files. @@ -664,9 +699,10 @@ class Bot(TelegramObject): if isinstance(document, Document): document = document.file_id elif InputFile.is_file(document): + document = cast(IO, document) document = InputFile(document, filename=filename) - data = {'chat_id': chat_id, 'document': document} + data: JSONDict = {'chat_id': chat_id, 'document': document} if caption: data['caption'] = caption @@ -674,23 +710,24 @@ class Bot(TelegramObject): data['parse_mode'] = parse_mode if thumb: if InputFile.is_file(thumb): + thumb = cast(IO, thumb) thumb = InputFile(thumb, attach=True) data['thumb'] = thumb - return self._message('sendDocument', data, timeout=timeout, + return self._message('sendDocument', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_sticker(self, - chat_id, - sticker, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - api_kwargs=None): + chat_id: Union[int, str], + sticker: Union[str, Sticker, FileLike], + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + api_kwargs: JSONDict = None) -> Optional[Message]: """ Use this method to send static .WEBP or animated .TGS stickers. @@ -727,31 +764,32 @@ class Bot(TelegramObject): if isinstance(sticker, Sticker): sticker = sticker.file_id elif InputFile.is_file(sticker): + sticker = cast(IO, sticker) sticker = InputFile(sticker) - data = {'chat_id': chat_id, 'sticker': sticker} + data: JSONDict = {'chat_id': chat_id, 'sticker': sticker} - return self._message('sendSticker', data, timeout=timeout, + return self._message('sendSticker', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_video(self, - chat_id, - video, - duration=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - width=None, - height=None, - parse_mode=None, - supports_streaming=None, - thumb=None, - api_kwargs=None): + chat_id: Union[int, str], + video: Union[str, Video, FileLike], + duration: int = None, + caption: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + width: int = None, + height: int = None, + parse_mode: str = None, + supports_streaming: bool = None, + thumb: FileLike = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -810,9 +848,10 @@ class Bot(TelegramObject): if isinstance(video, Video): video = video.file_id elif InputFile.is_file(video): + video = cast(IO, video) video = InputFile(video) - data = {'chat_id': chat_id, 'video': video} + data: JSONDict = {'chat_id': chat_id, 'video': video} if duration: data['duration'] = duration @@ -828,26 +867,28 @@ class Bot(TelegramObject): data['height'] = height if thumb: if InputFile.is_file(thumb): + thumb = cast(IO, thumb) thumb = InputFile(thumb, attach=True) data['thumb'] = thumb - return self._message('sendVideo', data, timeout=timeout, + return self._message('sendVideo', data, # type: ignore[return-value] + timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_video_note(self, - chat_id, - video_note, - duration=None, - length=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - thumb=None, - api_kwargs=None): + chat_id: Union[int, str], + video_note: Union[str, FileLike, VideoNote], + duration: int = None, + length: int = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + thumb: FileLike = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. @@ -896,9 +937,10 @@ class Bot(TelegramObject): if isinstance(video_note, VideoNote): video_note = video_note.file_id elif InputFile.is_file(video_note): + video_note = cast(IO, video_note) video_note = InputFile(video_note) - data = {'chat_id': chat_id, 'video_note': video_note} + data: JSONDict = {'chat_id': chat_id, 'video_note': video_note} if duration is not None: data['duration'] = duration @@ -906,29 +948,30 @@ class Bot(TelegramObject): data['length'] = length if thumb: if InputFile.is_file(thumb): + thumb = cast(IO, thumb) thumb = InputFile(thumb, attach=True) data['thumb'] = thumb - return self._message('sendVideoNote', data, timeout=timeout, + return self._message('sendVideoNote', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_animation(self, - chat_id, - animation, - duration=None, - width=None, - height=None, - thumb=None, - caption=None, - parse_mode=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - api_kwargs=None): + chat_id: Union[int, str], + animation: Union[str, FileLike, Animation], + duration: int = None, + width: int = None, + height: int = None, + thumb: FileLike = None, + caption: str = None, + parse_mode: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + api_kwargs: JSONDict = None) -> Optional[Message]: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). Bots can currently send animation files of up to 50 MB in size, this limit may be changed @@ -981,9 +1024,10 @@ class Bot(TelegramObject): if isinstance(animation, Animation): animation = animation.file_id elif InputFile.is_file(animation): + animation = cast(IO, animation) animation = InputFile(animation) - data = {'chat_id': chat_id, 'animation': animation} + data: JSONDict = {'chat_id': chat_id, 'animation': animation} if duration: data['duration'] = duration @@ -993,6 +1037,7 @@ class Bot(TelegramObject): data['height'] = height if thumb: if InputFile.is_file(thumb): + thumb = cast(IO, thumb) thumb = InputFile(thumb, attach=True) data['thumb'] = thumb if caption: @@ -1000,23 +1045,23 @@ class Bot(TelegramObject): if parse_mode: data['parse_mode'] = parse_mode - return self._message('sendAnimation', data, timeout=timeout, + return self._message('sendAnimation', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_voice(self, - chat_id, - voice, - duration=None, - caption=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=20, - parse_mode=None, - api_kwargs=None): + chat_id: Union[int, str], + voice: Union[str, FileLike, Voice], + duration: int = None, + caption: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = 20, + parse_mode: str = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """ Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .ogg file @@ -1062,9 +1107,10 @@ class Bot(TelegramObject): if isinstance(voice, Voice): voice = voice.file_id elif InputFile.is_file(voice): + voice = cast(IO, voice) voice = InputFile(voice) - data = {'chat_id': chat_id, 'voice': voice} + data: JSONDict = {'chat_id': chat_id, 'voice': voice} if duration: data['duration'] = duration @@ -1073,19 +1119,19 @@ class Bot(TelegramObject): if parse_mode: data['parse_mode'] = parse_mode - return self._message('sendVoice', data, timeout=timeout, + return self._message('sendVoice', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_media_group(self, - chat_id, - media, - disable_notification=None, - reply_to_message_id=None, - timeout=20, - api_kwargs=None): + chat_id: Union[int, str], + media: List[InputMedia], + disable_notification: bool = None, + reply_to_message_id: Union[int, str] = None, + timeout: float = 20, + api_kwargs: JSONDict = None) -> List[Optional[Message]]: """Use this method to send a group of photos or videos as an album. Args: @@ -1107,7 +1153,7 @@ class Bot(TelegramObject): Raises: :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'media': media} + data: JSONDict = {'chat_id': chat_id, 'media': media} for m in data['media']: if m.parse_mode == DEFAULT_NONE: @@ -1123,20 +1169,24 @@ class Bot(TelegramObject): result = self._post('sendMediaGroup', data, timeout=timeout, api_kwargs=api_kwargs) - return [Message.de_json(res, self) for res in result] + if self.defaults: + for res in result: # type: ignore + res['default_quote'] = self.defaults.quote # type: ignore + + return [Message.de_json(res, self) for res in result] # type: ignore @log def send_location(self, - chat_id, - latitude=None, - longitude=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - location=None, - live_period=None, - api_kwargs=None): + chat_id: Union[int, str], + latitude: float = None, + longitude: float = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + location: Location = None, + live_period: int = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """Use this method to send point on the map. Note: @@ -1182,27 +1232,27 @@ class Bot(TelegramObject): latitude = location.latitude longitude = location.longitude - data = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude} + data: JSONDict = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude} if live_period: data['live_period'] = live_period - return self._message('sendLocation', data, timeout=timeout, + return self._message('sendLocation', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def edit_message_live_location(self, - chat_id=None, - message_id=None, - inline_message_id=None, - latitude=None, - longitude=None, - location=None, - reply_markup=None, - timeout=None, - api_kwargs=None): + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + latitude: float = None, + longitude: float = None, + location: Location = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[Optional[Message], bool]: """Use this method to edit live location messages sent by the bot or via the bot (for inline bots). A location can be edited until its :attr:`live_period` expires or editing is explicitly disabled by a call to :attr:`stop_message_live_location`. @@ -1244,7 +1294,7 @@ class Bot(TelegramObject): latitude = location.latitude longitude = location.longitude - data = {'latitude': latitude, 'longitude': longitude} + data: JSONDict = {'latitude': latitude, 'longitude': longitude} if chat_id: data['chat_id'] = chat_id @@ -1258,12 +1308,12 @@ class Bot(TelegramObject): @log def stop_message_live_location(self, - chat_id=None, - message_id=None, - inline_message_id=None, - reply_markup=None, - timeout=None, - api_kwargs=None): + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[Optional[Message], bool]: """Use this method to stop updating a live location message sent by the bot or via the bot (for inline bots) before live_period expires. @@ -1285,9 +1335,9 @@ class Bot(TelegramObject): Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise :obj:`True` is returned. + sent Message is returned, otherwise :obj:`True` is returned. """ - data = {} + data: JSONDict = {} if chat_id: data['chat_id'] = chat_id @@ -1301,19 +1351,19 @@ class Bot(TelegramObject): @log def send_venue(self, - chat_id, - latitude=None, - longitude=None, - title=None, - address=None, - foursquare_id=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - venue=None, - foursquare_type=None, - api_kwargs=None): + chat_id: Union[int, str], + latitude: float = None, + longitude: float = None, + title: str = None, + address: str = None, + foursquare_id: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + venue: Venue = None, + foursquare_type: str = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """Use this method to send information about a venue. Note: @@ -1365,7 +1415,7 @@ class Bot(TelegramObject): foursquare_id = venue.foursquare_id foursquare_type = venue.foursquare_type - data = { + data: JSONDict = { 'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude, @@ -1378,24 +1428,24 @@ class Bot(TelegramObject): if foursquare_type: data['foursquare_type'] = foursquare_type - return self._message('sendVenue', data, timeout=timeout, + return self._message('sendVenue', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_contact(self, - chat_id, - phone_number=None, - first_name=None, - last_name=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - contact=None, - vcard=None, - api_kwargs=None): + chat_id: Union[int, str], + phone_number: str = None, + first_name: str = None, + last_name: str = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + contact: Contact = None, + vcard: str = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """Use this method to send phone contacts. Note: @@ -1441,27 +1491,28 @@ class Bot(TelegramObject): last_name = contact.last_name vcard = contact.vcard - data = {'chat_id': chat_id, 'phone_number': phone_number, 'first_name': first_name} + data: JSONDict = {'chat_id': chat_id, 'phone_number': phone_number, + 'first_name': first_name} if last_name: data['last_name'] = last_name if vcard: data['vcard'] = vcard - return self._message('sendContact', data, timeout=timeout, + return self._message('sendContact', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def send_game(self, - chat_id, - game_short_name, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - api_kwargs=None): + chat_id: Union[int, str], + game_short_name: str, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Optional[Message]: """Use this method to send a game. Args: @@ -1489,15 +1540,19 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'game_short_name': game_short_name} + data: JSONDict = {'chat_id': chat_id, 'game_short_name': game_short_name} - return self._message('sendGame', data, timeout=timeout, + return self._message('sendGame', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log - def send_chat_action(self, chat_id, action, timeout=None, api_kwargs=None): + def send_chat_action(self, + chat_id: Union[str, int], + action: ChatAction, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, @@ -1523,24 +1578,24 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'action': action} + data: JSONDict = {'chat_id': chat_id, 'action': action} result = self._post('sendChatAction', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log def answer_inline_query(self, - inline_query_id, - results, - cache_time=300, - is_personal=None, - next_offset=None, - switch_pm_text=None, - switch_pm_parameter=None, - timeout=None, - current_offset=None, - api_kwargs=None): + inline_query_id: str, + results: List[InlineQueryResult], + cache_time: int = 300, + is_personal: bool = None, + next_offset: str = None, + switch_pm_text: str = None, + switch_pm_parameter: str = None, + timeout: float = None, + current_offset: str = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to send answers to an inline query. No more than 50 results per query are allowed. @@ -1598,35 +1653,8 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - if current_offset is not None and next_offset is not None: - raise ValueError('`current_offset` and `next_offset` are mutually exclusive!') - - if current_offset is not None: - if current_offset == '': - current_offset = 0 - else: - current_offset = int(current_offset) - - next_offset = '' - - if callable(results): - effective_results = results(current_offset) - if not effective_results: - effective_results = [] - else: - next_offset = current_offset + 1 - else: - if len(results) > (current_offset + 1) * MAX_INLINE_QUERY_RESULTS: - next_offset = current_offset + 1 - effective_results = results[ - current_offset * MAX_INLINE_QUERY_RESULTS: - next_offset * MAX_INLINE_QUERY_RESULTS] - else: - effective_results = results[current_offset * MAX_INLINE_QUERY_RESULTS:] - else: - effective_results = results - - for res in effective_results: + @no_type_check + def _set_defaults(res): if res._has_parse_mode and res.parse_mode == DEFAULT_NONE: if self.defaults: res.parse_mode = self.defaults.parse_mode @@ -1647,8 +1675,42 @@ class Bot(TelegramObject): else: res.input_message_content.disable_web_page_preview = None - effective_results = [res.to_dict() for res in effective_results] - data = {'inline_query_id': inline_query_id, 'results': effective_results} + if current_offset is not None and next_offset is not None: + raise ValueError('`current_offset` and `next_offset` are mutually exclusive!') + + if current_offset is not None: + if current_offset == '': + current_offset_int = 0 + else: + current_offset_int = int(current_offset) + + next_offset = '' + + if callable(results): + effective_results = results(current_offset_int) + if not effective_results: + effective_results = [] + else: + next_offset = str(current_offset_int + 1) + else: + if len(results) > (current_offset_int + 1) * MAX_INLINE_QUERY_RESULTS: + next_offset_int = current_offset_int + 1 + next_offset = str(next_offset_int) + effective_results = results[ + current_offset_int * MAX_INLINE_QUERY_RESULTS: + next_offset_int * MAX_INLINE_QUERY_RESULTS] + else: + effective_results = results[current_offset_int * MAX_INLINE_QUERY_RESULTS:] + else: + effective_results = results + + for result in effective_results: + _set_defaults(result) + + results_dicts = [res.to_dict() for res in effective_results] + + data: JSONDict = {'inline_query_id': inline_query_id, 'results': results_dicts} + if cache_time or cache_time == 0: data['cache_time'] = cache_time if is_personal: @@ -1660,13 +1722,16 @@ class Bot(TelegramObject): if switch_pm_parameter: data['switch_pm_parameter'] = switch_pm_parameter - result = self._post('answerInlineQuery', data, timeout=timeout, api_kwargs=api_kwargs) - - return result + return self._post('answerInlineQuery', data, timeout=timeout, # type: ignore[return-value] + api_kwargs=api_kwargs) @log - def get_user_profile_photos(self, user_id, offset=None, limit=100, timeout=None, - api_kwargs=None): + def get_user_profile_photos(self, + user_id: Union[str, int], + offset: int = None, + limit: int = 100, + timeout: float = None, + api_kwargs: JSONDict = None) -> Optional[UserProfilePhotos]: """Use this method to get a list of profile pictures for a user. Args: @@ -1688,7 +1753,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'user_id': user_id} + data: JSONDict = {'user_id': user_id} if offset is not None: data['offset'] = offset @@ -1697,10 +1762,14 @@ class Bot(TelegramObject): result = self._post('getUserProfilePhotos', data, timeout=timeout, api_kwargs=api_kwargs) - return UserProfilePhotos.de_json(result, self) + return UserProfilePhotos.de_json(result, self) # type: ignore @log - def get_file(self, file_id, timeout=None, api_kwargs=None): + def get_file(self, + file_id: Union[str, Animation, Audio, ChatPhoto, Document, PhotoSize, Sticker, + Video, VideoNote, Voice], + timeout: float = None, + api_kwargs: JSONDict = None) -> File: """ Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. The file can then be downloaded @@ -1735,21 +1804,27 @@ class Bot(TelegramObject): """ try: - file_id = file_id.file_id + file_id = file_id.file_id # type: ignore[union-attr] except AttributeError: pass - data = {'file_id': file_id} + data: JSONDict = {'file_id': file_id} result = self._post('getFile', data, timeout=timeout, api_kwargs=api_kwargs) - if result.get('file_path'): - result['file_path'] = '{}/{}'.format(self.base_file_url, result['file_path']) + if result.get('file_path'): # type: ignore + result['file_path'] = '{}/{}'.format(self.base_file_url, # type: ignore + result['file_path']) # type: ignore - return File.de_json(result, self) + return File.de_json(result, self) # type: ignore @log - def kick_chat_member(self, chat_id, user_id, timeout=None, until_date=None, api_kwargs=None): + def kick_chat_member(self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: float = None, + until_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to kick a user from a group or a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group on their own @@ -1778,7 +1853,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'user_id': user_id} + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} if until_date is not None: if isinstance(until_date, datetime): @@ -1788,10 +1863,14 @@ class Bot(TelegramObject): result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def unban_chat_member(self, chat_id, user_id, timeout=None, api_kwargs=None): + def unban_chat_member(self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """Use this method to unban a previously kicked user in a supergroup or channel. The user will not return to the group automatically, but will be able to join via link, @@ -1814,21 +1893,21 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'user_id': user_id} + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} result = self._post('unbanChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log def answer_callback_query(self, - callback_query_id, - text=None, - show_alert=False, - url=None, - cache_time=None, - timeout=None, - api_kwargs=None): + callback_query_id: str, + text: str = None, + show_alert: bool = False, + url: str = None, + cache_time: int = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an @@ -1866,7 +1945,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'callback_query_id': callback_query_id} + data: JSONDict = {'callback_query_id': callback_query_id} if text: data['text'] = text @@ -1879,19 +1958,19 @@ class Bot(TelegramObject): result = self._post('answerCallbackQuery', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log def edit_message_text(self, - text, - chat_id=None, - message_id=None, - inline_message_id=None, - parse_mode=None, - disable_web_page_preview=None, - reply_markup=None, - timeout=None, - api_kwargs=None): + text: str, + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + parse_mode: str = None, + disable_web_page_preview: str = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[Optional[Message], bool]: """ Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). @@ -1926,7 +2005,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'text': text} + data: JSONDict = {'text': text} if chat_id: data['chat_id'] = chat_id @@ -1944,14 +2023,14 @@ class Bot(TelegramObject): @log def edit_message_caption(self, - chat_id=None, - message_id=None, - inline_message_id=None, - caption=None, - reply_markup=None, - timeout=None, - parse_mode=None, - api_kwargs=None): + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + caption: str = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + parse_mode: str = None, + api_kwargs: JSONDict = None) -> Union[Message, bool]: """ Use this method to edit captions of messages sent by the bot or via the bot (for inline bots). @@ -1990,7 +2069,7 @@ class Bot(TelegramObject): 'edit_message_caption: Both chat_id and message_id are required when ' 'inline_message_id is not specified') - data = {} + data: JSONDict = {} if caption: data['caption'] = caption @@ -2003,18 +2082,19 @@ class Bot(TelegramObject): if inline_message_id: data['inline_message_id'] = inline_message_id - return self._message('editMessageCaption', data, timeout=timeout, + return self._message('editMessageCaption', data, # type: ignore[return-value] + timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def edit_message_media(self, - chat_id=None, - message_id=None, - inline_message_id=None, - media=None, - reply_markup=None, - timeout=None, - api_kwargs=None): + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + media: InputMedia = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[Message, bool]: """ Use this method to edit animation, audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. @@ -2052,7 +2132,7 @@ class Bot(TelegramObject): 'edit_message_media: Both chat_id and message_id are required when ' 'inline_message_id is not specified') - data = {'media': media} + data: JSONDict = {'media': media} if chat_id: data['chat_id'] = chat_id @@ -2061,17 +2141,17 @@ class Bot(TelegramObject): if inline_message_id: data['inline_message_id'] = inline_message_id - return self._message('editMessageMedia', data, timeout=timeout, reply_markup=reply_markup, - api_kwargs=api_kwargs) + return self._message('editMessageMedia', data, # type: ignore[return-value] + timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def edit_message_reply_markup(self, - chat_id=None, - message_id=None, - inline_message_id=None, - reply_markup=None, - timeout=None, - api_kwargs=None): + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[Message, bool]: """ Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). @@ -2105,7 +2185,7 @@ class Bot(TelegramObject): 'edit_message_reply_markup: Both chat_id and message_id are required when ' 'inline_message_id is not specified') - data = {} + data: JSONDict = {} if chat_id: data['chat_id'] = chat_id @@ -2114,17 +2194,18 @@ class Bot(TelegramObject): if inline_message_id: data['inline_message_id'] = inline_message_id - return self._message('editMessageReplyMarkup', data, timeout=timeout, + return self._message('editMessageReplyMarkup', data, # type: ignore[return-value] + timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def get_updates(self, - offset=None, - limit=100, - timeout=0, - read_latency=2., - allowed_updates=None, - api_kwargs=None): + offset: int = None, + limit: int = 100, + timeout: float = 0, + read_latency: float = 2., + allowed_updates: List[str] = None, + api_kwargs: JSONDict = None) -> List[Update]: """Use this method to receive incoming updates using long polling. Args: @@ -2164,7 +2245,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'timeout': timeout} + data: JSONDict = {'timeout': timeout} if offset: data['offset'] = offset @@ -2182,20 +2263,25 @@ class Bot(TelegramObject): api_kwargs=api_kwargs) if result: - self.logger.debug('Getting updates: %s', [u['update_id'] for u in result]) + self.logger.debug('Getting updates: %s', + [u['update_id'] for u in result]) # type: ignore else: self.logger.debug('No new updates found.') - return [Update.de_json(u, self) for u in result] + if self.defaults: + for u in result: # type: ignore + u['default_quote'] = self.defaults.quote # type: ignore + + return [Update.de_json(u, self) for u in result] # type: ignore @log def set_webhook(self, - url=None, - certificate=None, - timeout=None, - max_connections=40, - allowed_updates=None, - api_kwargs=None): + url: str = None, + certificate: FileLike = None, + timeout: float = None, + max_connections: int = 40, + allowed_updates: List[str] = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, Telegram will send an HTTPS POST request to the @@ -2253,12 +2339,13 @@ class Bot(TelegramObject): .. _`guide to Webhooks`: https://core.telegram.org/bots/webhooks """ - data = {} + data: JSONDict = {} if url is not None: data['url'] = url if certificate: if InputFile.is_file(certificate): + certificate = cast(IO, certificate) certificate = InputFile(certificate) data['certificate'] = certificate if max_connections is not None: @@ -2268,10 +2355,10 @@ class Bot(TelegramObject): result = self._post('setWebhook', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def delete_webhook(self, timeout=None, api_kwargs=None): + def delete_webhook(self, timeout: float = None, api_kwargs: JSONDict = None) -> bool: """ Use this method to remove webhook integration if you decide to switch back to getUpdates. Requires no parameters. @@ -2292,10 +2379,13 @@ class Bot(TelegramObject): """ result = self._post('deleteWebhook', None, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def leave_chat(self, chat_id, timeout=None, api_kwargs=None): + def leave_chat(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """Use this method for your bot to leave a group, supergroup or channel. Args: @@ -2314,14 +2404,17 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('leaveChat', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def get_chat(self, chat_id, timeout=None, api_kwargs=None): + def get_chat(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> Chat: """ Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). @@ -2342,14 +2435,20 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('getChat', data, timeout=timeout, api_kwargs=api_kwargs) - return Chat.de_json(result, self) + if self.defaults: + result['default_quote'] = self.defaults.quote # type: ignore + + return Chat.de_json(result, self) # type: ignore @log - def get_chat_administrators(self, chat_id, timeout=None, api_kwargs=None): + def get_chat_administrators(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> List[ChatMember]: """ Use this method to get a list of administrators in a chat. @@ -2372,14 +2471,17 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('getChatAdministrators', data, timeout=timeout, api_kwargs=api_kwargs) - return [ChatMember.de_json(x, self) for x in result] + return [ChatMember.de_json(x, self) for x in result] # type: ignore @log - def get_chat_members_count(self, chat_id, timeout=None, api_kwargs=None): + def get_chat_members_count(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> int: """Use this method to get the number of members in a chat. Args: @@ -2398,14 +2500,18 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('getChatMembersCount', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def get_chat_member(self, chat_id, user_id, timeout=None, api_kwargs=None): + def get_chat_member(self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> ChatMember: """Use this method to get information about a member of a chat. Args: @@ -2425,14 +2531,18 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'user_id': user_id} + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} result = self._post('getChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return ChatMember.de_json(result, self) + return ChatMember.de_json(result, self) # type: ignore @log - def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, api_kwargs=None): + def set_chat_sticker_set(self, + chat_id: Union[str, int], + sticker_set_name: str, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """Use this method to set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned @@ -2452,14 +2562,17 @@ class Bot(TelegramObject): Returns: :obj:`bool`: On success, :obj:`True` is returned. """ - data = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name} + data: JSONDict = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name} result = self._post('setChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def delete_chat_sticker_set(self, chat_id, timeout=None, api_kwargs=None): + def delete_chat_sticker_set(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned in @@ -2477,13 +2590,15 @@ class Bot(TelegramObject): Returns: :obj:`bool`: On success, :obj:`True` is returned. """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('deleteChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] - def get_webhook_info(self, timeout=None, api_kwargs=None): + def get_webhook_info(self, + timeout: float = None, + api_kwargs: JSONDict = None) -> WebhookInfo: """Use this method to get current webhook status. Requires no parameters. If the bot is using getUpdates, will return an object with the url field empty. @@ -2501,19 +2616,19 @@ class Bot(TelegramObject): """ result = self._post('getWebhookInfo', None, timeout=timeout, api_kwargs=api_kwargs) - return WebhookInfo.de_json(result, self) + return WebhookInfo.de_json(result, self) # type: ignore @log def set_game_score(self, - user_id, - score, - chat_id=None, - message_id=None, - inline_message_id=None, - force=None, - disable_edit_message=None, - timeout=None, - api_kwargs=None): + user_id: Union[int, str], + score: int, + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + force: bool = None, + disable_edit_message: bool = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Union[Message, bool]: """ Use this method to set the score of the specified user in a game. @@ -2545,7 +2660,7 @@ class Bot(TelegramObject): current score in the chat and force is :obj:`False`. """ - data = {'user_id': user_id, 'score': score} + data: JSONDict = {'user_id': user_id, 'score': score} if chat_id: data['chat_id'] = chat_id @@ -2558,16 +2673,17 @@ class Bot(TelegramObject): if disable_edit_message is not None: data['disable_edit_message'] = disable_edit_message - return self._message('setGameScore', data, timeout=timeout, api_kwargs=api_kwargs) + return self._message('setGameScore', data, timeout=timeout, # type: ignore[return-value] + api_kwargs=api_kwargs) @log def get_game_high_scores(self, - user_id, - chat_id=None, - message_id=None, - inline_message_id=None, - timeout=None, - api_kwargs=None): + user_id: Union[int, str], + chat_id: Union[str, int] = None, + message_id: Union[str, int] = None, + inline_message_id: Union[str, int] = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> List[GameHighScore]: """ Use this method to get data for high score tables. Will return the score of the specified user and several of his neighbors in a game. @@ -2593,7 +2709,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'user_id': user_id} + data: JSONDict = {'user_id': user_id} if chat_id: data['chat_id'] = chat_id @@ -2604,35 +2720,35 @@ class Bot(TelegramObject): result = self._post('getGameHighScores', data, timeout=timeout, api_kwargs=api_kwargs) - return [GameHighScore.de_json(hs, self) for hs in result] + return [GameHighScore.de_json(hs, self) for hs in result] # type: ignore @log def send_invoice(self, - chat_id, - title, - description, - payload, - provider_token, - start_parameter, - currency, - prices, - photo_url=None, - photo_size=None, - photo_width=None, - photo_height=None, - need_name=None, - need_phone_number=None, - need_email=None, - need_shipping_address=None, - is_flexible=None, - disable_notification=False, - reply_to_message_id=None, - reply_markup=None, - provider_data=None, - send_phone_number_to_provider=None, - send_email_to_provider=None, - timeout=None, - api_kwargs=None): + chat_id: Union[int, str], + title: str, + description: str, + payload: str, + provider_token: str, + start_parameter: str, + currency: str, + prices: List[LabeledPrice], + photo_url: str = None, + photo_size: int = None, + photo_width: int = None, + photo_height: int = None, + need_name: bool = None, + need_phone_number: bool = None, + need_email: bool = None, + need_shipping_address: bool = None, + is_flexible: bool = None, + disable_notification: bool = False, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + provider_data: Union[str, object] = None, + send_phone_number_to_provider: bool = None, + send_email_to_provider: bool = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Message: """Use this method to send invoices. Args: @@ -2693,7 +2809,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = { + data: JSONDict = { 'chat_id': chat_id, 'title': title, 'description': description, @@ -2731,19 +2847,19 @@ class Bot(TelegramObject): if send_email_to_provider is not None: data['send_email_to_provider'] = send_email_to_provider - return self._message('sendInvoice', data, timeout=timeout, + return self._message('sendInvoice', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def answer_shipping_query(self, - shipping_query_id, - ok, - shipping_options=None, - error_message=None, - timeout=None, - api_kwargs=None): + shipping_query_id: str, + ok: bool, + shipping_options: List[ShippingOption] = None, + error_message: str = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a shipping_query field to the bot. Use @@ -2785,20 +2901,25 @@ class Bot(TelegramObject): 'answerShippingQuery: If ok is False, error_message ' 'should not be empty and there should not be shipping_options') - data = {'shipping_query_id': shipping_query_id, 'ok': ok} + data: JSONDict = {'shipping_query_id': shipping_query_id, 'ok': ok} if ok: + assert shipping_options data['shipping_options'] = [option.to_dict() for option in shipping_options] if error_message is not None: data['error_message'] = error_message result = self._post('answerShippingQuery', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def answer_pre_checkout_query(self, pre_checkout_query_id, ok, - error_message=None, timeout=None, api_kwargs=None): + def answer_pre_checkout_query(self, + pre_checkout_query_id: str, + ok: bool, + error_message: str = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query. Use this method to @@ -2839,18 +2960,23 @@ class Bot(TelegramObject): 'not be error_message; if ok is False, error_message ' 'should not be empty') - data = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok} + data: JSONDict = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok} if error_message is not None: data['error_message'] = error_message result = self._post('answerPreCheckoutQuery', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def restrict_chat_member(self, chat_id, user_id, permissions, until_date=None, - timeout=None, api_kwargs=None): + def restrict_chat_member(self, + chat_id: Union[str, int], + user_id: Union[str, int], + permissions: ChatPermissions, + until_date: Union[int, datetime] = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. Pass @@ -2885,7 +3011,8 @@ class Bot(TelegramObject): Raises: :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'user_id': user_id, 'permissions': permissions.to_dict()} + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id, + 'permissions': permissions.to_dict()} if until_date is not None: if isinstance(until_date, datetime): @@ -2895,14 +3022,22 @@ class Bot(TelegramObject): result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def promote_chat_member(self, chat_id, user_id, can_change_info=None, - can_post_messages=None, can_edit_messages=None, - can_delete_messages=None, can_invite_users=None, - can_restrict_members=None, can_pin_messages=None, - can_promote_members=None, timeout=None, api_kwargs=None): + def promote_chat_member(self, + chat_id: Union[str, int], + user_id: Union[str, int], + can_change_info: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_invite_users: bool = None, + can_restrict_members: bool = None, + can_pin_messages: bool = None, + can_promote_members: bool = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -2943,7 +3078,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'user_id': user_id} + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} if can_change_info is not None: data['can_change_info'] = can_change_info @@ -2964,10 +3099,14 @@ class Bot(TelegramObject): result = self._post('promoteChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_chat_permissions(self, chat_id, permissions, timeout=None, api_kwargs=None): + def set_chat_permissions(self, + chat_id: Union[str, int], + permissions: ChatPermissions, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to set default chat permissions for all members. The bot must be an administrator in the group or a supergroup for this to work and must have the @@ -2990,19 +3129,19 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'permissions': permissions.to_dict()} + data: JSONDict = {'chat_id': chat_id, 'permissions': permissions.to_dict()} result = self._post('setChatPermissions', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log def set_chat_administrator_custom_title(self, - chat_id, - user_id, - custom_title, - timeout=None, - api_kwargs=None): + chat_id: Union[int, str], + user_id: Union[int, str], + custom_title: str, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to set a custom title for administrators promoted by the bot in a supergroup. The bot must be an administrator for this to work. @@ -3026,15 +3165,19 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'user_id': user_id, 'custom_title': custom_title} + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id, + 'custom_title': custom_title} result = self._post('setChatAdministratorCustomTitle', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def export_chat_invite_link(self, chat_id, timeout=None, api_kwargs=None): + def export_chat_invite_link(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> str: """ Use this method to generate a new invite link for a chat; any previously generated link is revoked. The bot must be an administrator in the chat for this to work and must have @@ -3056,14 +3199,18 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('exportChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_chat_photo(self, chat_id, photo, timeout=20, api_kwargs=None): + def set_chat_photo(self, + chat_id: Union[str, int], + photo: FileLike, + timeout: float = 20, + api_kwargs: JSONDict = None) -> bool: """Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat @@ -3087,16 +3234,20 @@ class Bot(TelegramObject): """ if InputFile.is_file(photo): + photo = cast(IO, photo) photo = InputFile(photo) - data = {'chat_id': chat_id, 'photo': photo} + data: JSONDict = {'chat_id': chat_id, 'photo': photo} result = self._post('setChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def delete_chat_photo(self, chat_id, timeout=None, api_kwargs=None): + def delete_chat_photo(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin @@ -3118,14 +3269,18 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('deleteChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_chat_title(self, chat_id, title, timeout=None, api_kwargs=None): + def set_chat_title(self, + chat_id: Union[str, int], + title: str, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate @@ -3148,14 +3303,18 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'title': title} + data: JSONDict = {'chat_id': chat_id, 'title': title} result = self._post('setChatTitle', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_chat_description(self, chat_id, description, timeout=None, api_kwargs=None): + def set_chat_description(self, + chat_id: Union[str, int], + description: str, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to change the description of a group, a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin @@ -3178,15 +3337,19 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'description': description} + data: JSONDict = {'chat_id': chat_id, 'description': description} result = self._post('setChatDescription', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def pin_chat_message(self, chat_id, message_id, disable_notification=None, timeout=None, - api_kwargs=None): + def pin_chat_message(self, + chat_id: Union[str, int], + message_id: Union[str, int], + disable_notification: bool = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to pin a message in a group, a supergroup, or a channel. The bot must be an administrator in the chat for this to work and must have the @@ -3213,17 +3376,20 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id, 'message_id': message_id} + data: JSONDict = {'chat_id': chat_id, 'message_id': message_id} if disable_notification is not None: data['disable_notification'] = disable_notification result = self._post('pinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def unpin_chat_message(self, chat_id, timeout=None, api_kwargs=None): + def unpin_chat_message(self, + chat_id: Union[str, int], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to unpin a message in a group, a supergroup, or a channel. The bot must be an administrator in the chat for this to work and must have the @@ -3246,14 +3412,17 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'chat_id': chat_id} + data: JSONDict = {'chat_id': chat_id} result = self._post('unpinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def get_sticker_set(self, name, timeout=None, api_kwargs=None): + def get_sticker_set(self, + name: str, + timeout: float = None, + api_kwargs: JSONDict = None) -> StickerSet: """Use this method to get a sticker set. Args: @@ -3271,14 +3440,18 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'name': name} + data: JSONDict = {'name': name} result = self._post('getStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - return StickerSet.de_json(result, self) + return StickerSet.de_json(result, self) # type: ignore @log - def upload_sticker_file(self, user_id, png_sticker, timeout=20, api_kwargs=None): + def upload_sticker_file(self, + user_id: Union[str, int], + png_sticker: Union[str, FileLike], + timeout: float = 20, + api_kwargs: JSONDict = None) -> File: """ Use this method to upload a .png file with a sticker for later use in :attr:`create_new_sticker_set` and :attr:`add_sticker_to_set` methods (can be used multiple @@ -3307,18 +3480,26 @@ class Bot(TelegramObject): """ if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) + png_sticker = InputFile(png_sticker) # type: ignore[assignment,arg-type] - data = {'user_id': user_id, 'png_sticker': png_sticker} + data: JSONDict = {'user_id': user_id, 'png_sticker': png_sticker} result = self._post('uploadStickerFile', data, timeout=timeout, api_kwargs=api_kwargs) - return File.de_json(result, self) + return File.de_json(result, self) # type: ignore @log - def create_new_sticker_set(self, user_id, name, title, emojis, png_sticker=None, - contains_masks=None, mask_position=None, timeout=20, - tgs_sticker=None, api_kwargs=None): + def create_new_sticker_set(self, + user_id: Union[str, int], + name: str, + title: str, + emojis: str, + png_sticker: Union[str, FileLike] = None, + contains_masks: bool = None, + mask_position: MaskPosition = None, + timeout: float = 20, + tgs_sticker: Union[str, FileLike] = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. @@ -3370,12 +3551,12 @@ class Bot(TelegramObject): """ if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) + png_sticker = InputFile(png_sticker) # type: ignore[assignment,arg-type] if InputFile.is_file(tgs_sticker): - tgs_sticker = InputFile(tgs_sticker) + tgs_sticker = InputFile(tgs_sticker) # type: ignore[assignment,arg-type] - data = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis} + data: JSONDict = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis} if png_sticker is not None: data['png_sticker'] = png_sticker @@ -3390,11 +3571,18 @@ class Bot(TelegramObject): result = self._post('createNewStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def add_sticker_to_set(self, user_id, name, emojis, png_sticker=None, mask_position=None, - timeout=20, tgs_sticker=None, api_kwargs=None): + def add_sticker_to_set(self, + user_id: Union[str, int], + name: str, + emojis: str, + png_sticker: Union[str, FileLike] = None, + mask_position: MaskPosition = None, + timeout: float = 20, + tgs_sticker: Union[str, FileLike] = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to add a new sticker to a set created by the bot. You must use exactly one of the fields png_sticker or tgs_sticker. Animated stickers @@ -3440,12 +3628,12 @@ class Bot(TelegramObject): """ if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) + png_sticker = InputFile(png_sticker) # type: ignore[assignment,arg-type] if InputFile.is_file(tgs_sticker): - tgs_sticker = InputFile(tgs_sticker) + tgs_sticker = InputFile(tgs_sticker) # type: ignore[assignment,arg-type] - data = {'user_id': user_id, 'name': name, 'emojis': emojis} + data: JSONDict = {'user_id': user_id, 'name': name, 'emojis': emojis} if png_sticker is not None: data['png_sticker'] = png_sticker @@ -3458,10 +3646,14 @@ class Bot(TelegramObject): result = self._post('addStickerToSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_sticker_position_in_set(self, sticker, position, timeout=None, api_kwargs=None): + def set_sticker_position_in_set(self, + sticker: str, + position: int, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """Use this method to move a sticker in a set created by the bot to a specific position. Args: @@ -3480,15 +3672,18 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'sticker': sticker, 'position': position} + data: JSONDict = {'sticker': sticker, 'position': position} result = self._post('setStickerPositionInSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def delete_sticker_from_set(self, sticker, timeout=None, api_kwargs=None): + def delete_sticker_from_set(self, + sticker: str, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """Use this method to delete a sticker from a set created by the bot. Args: @@ -3506,14 +3701,19 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'sticker': sticker} + data: JSONDict = {'sticker': sticker} result = self._post('deleteStickerFromSet', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_sticker_set_thumb(self, name, user_id, thumb=None, timeout=None, api_kwargs=None): + def set_sticker_set_thumb(self, + name: str, + user_id: Union[str, int], + thumb: FileLike = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. @@ -3546,16 +3746,21 @@ class Bot(TelegramObject): """ if InputFile.is_file(thumb): + thumb = cast(IO, thumb) thumb = InputFile(thumb) - data = {'name': name, 'user_id': user_id, 'thumb': thumb} + data: JSONDict = {'name': name, 'user_id': user_id, 'thumb': thumb} result = self._post('setStickerSetThumb', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log - def set_passport_data_errors(self, user_id, errors, timeout=None, api_kwargs=None): + def set_passport_data_errors(self, + user_id: Union[str, int], + errors: List[PassportElementError], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Informs a user that some of the Telegram Passport elements they provided contains errors. The user will not be able to re-submit their Passport to you until the errors are fixed @@ -3583,31 +3788,32 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = {'user_id': user_id, 'errors': [error.to_dict() for error in errors]} + data: JSONDict = {'user_id': user_id, + 'errors': [error.to_dict() for error in errors]} result = self._post('setPassportDataErrors', data, timeout=timeout, api_kwargs=api_kwargs) - return result + return result # type: ignore[return-value] @log def send_poll(self, - chat_id, - question, - options, - is_anonymous=True, - type=Poll.REGULAR, - allows_multiple_answers=False, - correct_option_id=None, - is_closed=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - explanation=None, - explanation_parse_mode=DEFAULT_NONE, - open_period=None, - close_date=None, - api_kwargs=None): + chat_id: Union[int, str], + question: str, + options: List[str], + is_anonymous: bool = True, + type: str = Poll.REGULAR, + allows_multiple_answers: bool = False, + correct_option_id: int = None, + is_closed: bool = None, + disable_notification: bool = None, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + explanation: str = None, + explanation_parse_mode: Union[str, DefaultValue, None] = DEFAULT_NONE, + open_period: int = None, + close_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None) -> Message: """ Use this method to send a native poll. @@ -3660,7 +3866,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = { + data: JSONDict = { 'chat_id': chat_id, 'question': question, 'options': options @@ -3694,18 +3900,18 @@ class Bot(TelegramObject): tzinfo=self.defaults.tzinfo if self.defaults else None) data['close_date'] = close_date - return self._message('sendPoll', data, timeout=timeout, + return self._message('sendPoll', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log def stop_poll(self, - chat_id, - message_id, - reply_markup=None, - timeout=None, - api_kwargs=None): + chat_id: Union[int, str], + message_id: Union[int, str], + reply_markup: ReplyMarkup = None, + timeout: float = None, + api_kwargs: JSONDict = None) -> Poll: """ Use this method to stop a poll which was sent by the bot. @@ -3729,7 +3935,7 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = { + data: JSONDict = { 'chat_id': chat_id, 'message_id': message_id } @@ -3744,17 +3950,17 @@ class Bot(TelegramObject): result = self._post('stopPoll', data, timeout=timeout, api_kwargs=api_kwargs) - return Poll.de_json(result, self) + return Poll.de_json(result, self) # type: ignore @log def send_dice(self, - chat_id, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None, - timeout=None, - emoji=None, - api_kwargs=None): + chat_id: Union[int, str], + disable_notification: bool = None, + reply_to_message_id: Union[int, str] = None, + reply_markup: ReplyMarkup = None, + timeout: float = None, + emoji: str = None, + api_kwargs: JSONDict = None) -> Message: """ Use this method to send an animated emoji, which will have a random value. On success, the sent Message is returned. @@ -3784,20 +3990,22 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data = { + data: JSONDict = { 'chat_id': chat_id, } if emoji: data['emoji'] = emoji - return self._message('sendDice', data, timeout=timeout, + return self._message('sendDice', data, timeout=timeout, # type: ignore[return-value] disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, api_kwargs=api_kwargs) @log - def get_my_commands(self, timeout=None, api_kwargs=None): + def get_my_commands(self, + timeout: float = None, + api_kwargs: JSONDict = None) -> List[BotCommand]: """ Use this method to get the current list of the bot's commands. @@ -3817,12 +4025,15 @@ class Bot(TelegramObject): """ result = self._post('getMyCommands', timeout=timeout, api_kwargs=api_kwargs) - self._commands = [BotCommand.de_json(c, self) for c in result] + self._commands = [BotCommand.de_json(c, self) for c in result] # type: ignore return self._commands @log - def set_my_commands(self, commands, timeout=None, api_kwargs=None): + def set_my_commands(self, + commands: List[Union[BotCommand, Tuple[str, str]]], + timeout: float = None, + api_kwargs: JSONDict = None) -> bool: """ Use this method to change the list of the bot's commands. @@ -3845,18 +4056,19 @@ class Bot(TelegramObject): """ cmds = [c if isinstance(c, BotCommand) else BotCommand(c[0], c[1]) for c in commands] - data = {'commands': [c.to_dict() for c in cmds]} + data: JSONDict = {'commands': [c.to_dict() for c in cmds]} result = self._post('setMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) # Set commands. No need to check for outcome. # If request failed, we won't come this far - self._commands = commands + self._commands = cmds - return result + return result # type: ignore[return-value] - def to_dict(self): - data = {'id': self.id, 'username': self.username, 'first_name': self.first_name} + def to_dict(self) -> JSONDict: + data: JSONDict = {'id': self.id, 'username': self.username, + 'first_name': self.first_name} if self.last_name: data['last_name'] = self.last_name diff --git a/telegram/botcommand.py b/telegram/botcommand.py index 560826f8c..0b780b229 100644 --- a/telegram/botcommand.py +++ b/telegram/botcommand.py @@ -19,6 +19,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Bot Command.""" from telegram import TelegramObject +from typing import Any class BotCommand(TelegramObject): @@ -37,15 +38,8 @@ class BotCommand(TelegramObject): English letters, digits and underscores. description (:obj:`str`): Description of the command, 3-256 characters. """ - def __init__(self, command, description, **kwargs): + def __init__(self, command: str, description: str, **kwargs: Any): self.command = command self.description = description self._id_attrs = (self.command, self.description) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegram/callbackquery.py b/telegram/callbackquery.py index 7e8e6b28f..1654e01e7 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -17,9 +17,14 @@ # 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 an object that represents a Telegram CallbackQuery""" - from telegram import TelegramObject, Message, User +from telegram.utils.types import JSONDict +from typing import Optional, Any, Union, TYPE_CHECKING, List + +if TYPE_CHECKING: + from telegram import Bot, InlineKeyboardMarkup, GameHighScore + class CallbackQuery(TelegramObject): """ @@ -74,15 +79,15 @@ class CallbackQuery(TelegramObject): """ def __init__(self, - id, - from_user, - chat_instance, - message=None, - data=None, - inline_message_id=None, - game_short_name=None, - bot=None, - **kwargs): + id: str, + from_user: User, + chat_instance: str, + message: Message = None, + data: str = None, + inline_message_id: str = None, + game_short_name: str = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.id = id self.from_user = from_user @@ -98,18 +103,18 @@ class CallbackQuery(TelegramObject): self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['CallbackQuery']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['from_user'] = User.de_json(data.get('from'), bot) data['message'] = Message.de_json(data.get('message'), bot) return cls(bot=bot, **data) - def answer(self, *args, **kwargs): + def answer(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.answer_callback_query(update.callback_query.id, *args, **kwargs) @@ -118,9 +123,9 @@ class CallbackQuery(TelegramObject): :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.answerCallbackQuery(self.id, *args, **kwargs) + return self.bot.answer_callback_query(self.id, *args, **kwargs) - def edit_message_text(self, text, *args, **kwargs): + def edit_message_text(self, text: str, *args: Any, **kwargs: Any) -> Union[Message, bool]: """Shortcut for either:: bot.edit_message_text(text, chat_id=update.callback_query.message.chat_id, @@ -144,7 +149,8 @@ class CallbackQuery(TelegramObject): return self.bot.edit_message_text(text, chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs) - def edit_message_caption(self, caption, *args, **kwargs): + def edit_message_caption(self, caption: str, *args: Any, + **kwargs: Any) -> Union[Message, bool]: """Shortcut for either:: bot.edit_message_caption(caption=caption, @@ -172,7 +178,8 @@ class CallbackQuery(TelegramObject): message_id=self.message.message_id, *args, **kwargs) - def edit_message_reply_markup(self, reply_markup, *args, **kwargs): + def edit_message_reply_markup(self, reply_markup: 'InlineKeyboardMarkup', *args: Any, + **kwargs: Any) -> Union[Message, bool]: """Shortcut for either:: bot.edit_message_reply_markup(chat_id=update.callback_query.message.chat_id, @@ -201,7 +208,7 @@ class CallbackQuery(TelegramObject): message_id=self.message.message_id, *args, **kwargs) - def edit_message_media(self, *args, **kwargs): + def edit_message_media(self, *args: Any, **kwargs: Any) -> Union[Message, bool]: """Shortcut for either:: bot.edit_message_media(chat_id=update.callback_query.message.chat_id, @@ -228,7 +235,7 @@ class CallbackQuery(TelegramObject): message_id=self.message.message_id, *args, **kwargs) - def edit_message_live_location(self, *args, **kwargs): + def edit_message_live_location(self, *args: Any, **kwargs: Any) -> Union[Message, bool]: """Shortcut for either:: bot.edit_message_live_location(chat_id=update.callback_query.message.chat_id, @@ -257,7 +264,7 @@ class CallbackQuery(TelegramObject): message_id=self.message.message_id, *args, **kwargs) - def stop_message_live_location(self, *args, **kwargs): + def stop_message_live_location(self, *args: Any, **kwargs: Any) -> Union[Message, bool]: """Shortcut for either:: bot.stop_message_live_location(chat_id=update.callback_query.message.chat_id, @@ -286,7 +293,7 @@ class CallbackQuery(TelegramObject): message_id=self.message.message_id, *args, **kwargs) - def set_game_score(self, *args, **kwargs): + def set_game_score(self, *args: Any, **kwargs: Any) -> Union[Message, bool]: """Shortcut for either:: bot.set_game_score(chat_id=update.callback_query.message.chat_id, @@ -313,7 +320,7 @@ class CallbackQuery(TelegramObject): message_id=self.message.message_id, *args, **kwargs) - def get_game_high_scores(self, *args, **kwargs): + def get_game_high_scores(self, *args: Any, **kwargs: Any) -> List['GameHighScore']: """Shortcut for either:: bot.get_game_high_scores(chat_id=update.callback_query.message.chat_id, @@ -328,8 +335,7 @@ class CallbackQuery(TelegramObject): *args, **kwargs) Returns: - :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise :obj:`True` is returned. + List[:class:`telegram.GameHighScore`] """ if self.inline_message_id: diff --git a/telegram/chat.py b/telegram/chat.py index a7e781f74..8f7ebe277 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -22,6 +22,11 @@ from telegram import TelegramObject, ChatPhoto from .chatpermissions import ChatPermissions +from telegram.utils.types import JSONDict +from typing import Any, Optional, List, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, Message, ChatMember + class Chat(TelegramObject): """This object represents a chat. @@ -41,7 +46,7 @@ class Chat(TelegramObject): invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups. Returned only in :meth:`telegram.Bot.get_chat`. - permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions, + permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user. Returned only in @@ -72,7 +77,7 @@ class Chat(TelegramObject): in :meth:`telegram.Bot.get_chat`. pinned_message (:class:`telegram.Message`, optional): Pinned message, for groups, supergroups and channels. Returned only in :meth:`telegram.Bot.get_chat`. - permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions, + permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user. @@ -86,32 +91,32 @@ class Chat(TelegramObject): """ - PRIVATE = 'private' + PRIVATE: str = 'private' """:obj:`str`: 'private'""" - GROUP = 'group' + GROUP: str = 'group' """:obj:`str`: 'group'""" - SUPERGROUP = 'supergroup' + SUPERGROUP: str = 'supergroup' """:obj:`str`: 'supergroup'""" - CHANNEL = 'channel' + CHANNEL: str = 'channel' """:obj:`str`: 'channel'""" def __init__(self, - id, - type, - title=None, - username=None, - first_name=None, - last_name=None, - bot=None, - photo=None, - description=None, - invite_link=None, - pinned_message=None, - permissions=None, - sticker_set_name=None, - can_set_sticker_set=None, - slow_mode_delay=None, - **kwargs): + id: int, + type: str, + title: str = None, + username: str = None, + first_name: str = None, + last_name: str = None, + bot: 'Bot' = None, + photo: ChatPhoto = None, + description: str = None, + invite_link: str = None, + pinned_message: 'Message' = None, + permissions: ChatPermissions = None, + sticker_set_name: str = None, + can_set_sticker_set: bool = None, + slow_mode_delay: int = None, + **kwargs: Any): # Required self.id = int(id) self.type = type @@ -135,7 +140,7 @@ class Chat(TelegramObject): self._id_attrs = (self.id,) @property - def link(self): + def link(self) -> Optional[str]: """:obj:`str`: Convenience property. If the chat has a :attr:`username`, returns a t.me link of the chat.""" if self.username: @@ -143,7 +148,9 @@ class Chat(TelegramObject): return None @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: JSONDict, bot: 'Bot') -> Optional['Chat']: + data = cls.parse_data(data) + if not data: return None @@ -154,7 +161,7 @@ class Chat(TelegramObject): return cls(bot=bot, **data) - def leave(self, *args, **kwargs): + def leave(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.leave_chat(update.effective_chat.id, *args, **kwargs) @@ -165,7 +172,7 @@ class Chat(TelegramObject): """ return self.bot.leave_chat(self.id, *args, **kwargs) - def get_administrators(self, *args, **kwargs): + def get_administrators(self, *args: Any, **kwargs: Any) -> List['ChatMember']: """Shortcut for:: bot.get_chat_administrators(update.effective_chat.id, *args, **kwargs) @@ -179,7 +186,7 @@ class Chat(TelegramObject): """ return self.bot.get_chat_administrators(self.id, *args, **kwargs) - def get_members_count(self, *args, **kwargs): + def get_members_count(self, *args: Any, **kwargs: Any) -> int: """Shortcut for:: bot.get_chat_members_count(update.effective_chat.id, *args, **kwargs) @@ -190,7 +197,7 @@ class Chat(TelegramObject): """ return self.bot.get_chat_members_count(self.id, *args, **kwargs) - def get_member(self, *args, **kwargs): + def get_member(self, *args: Any, **kwargs: Any) -> 'ChatMember': """Shortcut for:: bot.get_chat_member(update.effective_chat.id, *args, **kwargs) @@ -201,7 +208,7 @@ class Chat(TelegramObject): """ return self.bot.get_chat_member(self.id, *args, **kwargs) - def kick_member(self, *args, **kwargs): + def kick_member(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.kick_chat_member(update.effective_chat.id, *args, **kwargs) @@ -217,7 +224,7 @@ class Chat(TelegramObject): """ return self.bot.kick_chat_member(self.id, *args, **kwargs) - def unban_member(self, *args, **kwargs): + def unban_member(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.unban_chat_member(update.effective_chat.id, *args, **kwargs) @@ -228,18 +235,18 @@ class Chat(TelegramObject): """ return self.bot.unban_chat_member(self.id, *args, **kwargs) - def set_permissions(self, *args, **kwargs): + def set_permissions(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.set_chat_permissions(update.effective_chat.id, *args, **kwargs) Returns: - :obj:`bool`: If the action was sent successfully. + :obj:`bool`: If the action was sent successfully. """ return self.bot.set_chat_permissions(self.id, *args, **kwargs) - def set_administrator_custom_title(self, *args, **kwargs): + def set_administrator_custom_title(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.set_chat_administrator_custom_title(update.effective_chat.id, *args, **kwargs) @@ -250,7 +257,7 @@ class Chat(TelegramObject): """ return self.bot.set_chat_administrator_custom_title(self.id, *args, **kwargs) - def send_message(self, *args, **kwargs): + def send_message(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_message(update.effective_chat.id, *args, **kwargs) @@ -261,7 +268,7 @@ class Chat(TelegramObject): """ return self.bot.send_message(self.id, *args, **kwargs) - def send_media_group(self, *args, **kwargs): + def send_media_group(self, *args: Any, **kwargs: Any) -> List['Message']: """Shortcut for:: bot.send_media_group(update.effective_chat.id, *args, **kwargs) @@ -272,7 +279,7 @@ class Chat(TelegramObject): """ return self.bot.send_media_group(self.id, *args, **kwargs) - def send_chat_action(self, *args, **kwargs): + def send_chat_action(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.send_chat_action(update.effective_chat.id, *args, **kwargs) @@ -286,7 +293,7 @@ class Chat(TelegramObject): send_action = send_chat_action """Alias for :attr:`send_chat_action`""" - def send_photo(self, *args, **kwargs): + def send_photo(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_photo(update.effective_chat.id, *args, **kwargs) @@ -297,7 +304,7 @@ class Chat(TelegramObject): """ return self.bot.send_photo(self.id, *args, **kwargs) - def send_contact(self, *args, **kwargs): + def send_contact(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_contact(update.effective_chat.id, *args, **kwargs) @@ -308,7 +315,7 @@ class Chat(TelegramObject): """ return self.bot.send_contact(self.id, *args, **kwargs) - def send_audio(self, *args, **kwargs): + def send_audio(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_audio(update.effective_chat.id, *args, **kwargs) @@ -319,7 +326,7 @@ class Chat(TelegramObject): """ return self.bot.send_audio(self.id, *args, **kwargs) - def send_document(self, *args, **kwargs): + def send_document(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_document(update.effective_chat.id, *args, **kwargs) @@ -330,7 +337,7 @@ class Chat(TelegramObject): """ return self.bot.send_document(self.id, *args, **kwargs) - def send_dice(self, *args, **kwargs): + def send_dice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_dice(update.effective_chat.id, *args, **kwargs) @@ -341,7 +348,7 @@ class Chat(TelegramObject): """ return self.bot.send_dice(self.id, *args, **kwargs) - def send_game(self, *args, **kwargs): + def send_game(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_game(update.effective_chat.id, *args, **kwargs) @@ -352,7 +359,7 @@ class Chat(TelegramObject): """ return self.bot.send_game(self.id, *args, **kwargs) - def send_invoice(self, *args, **kwargs): + def send_invoice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_invoice(update.effective_chat.id, *args, **kwargs) @@ -363,7 +370,7 @@ class Chat(TelegramObject): """ return self.bot.send_invoice(self.id, *args, **kwargs) - def send_location(self, *args, **kwargs): + def send_location(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_location(update.effective_chat.id, *args, **kwargs) @@ -374,7 +381,7 @@ class Chat(TelegramObject): """ return self.bot.send_location(self.id, *args, **kwargs) - def send_animation(self, *args, **kwargs): + def send_animation(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_animation(update.effective_chat.id, *args, **kwargs) @@ -385,7 +392,7 @@ class Chat(TelegramObject): """ return self.bot.send_animation(self.id, *args, **kwargs) - def send_sticker(self, *args, **kwargs): + def send_sticker(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_sticker(update.effective_chat.id, *args, **kwargs) @@ -396,7 +403,7 @@ class Chat(TelegramObject): """ return self.bot.send_sticker(self.id, *args, **kwargs) - def send_venue(self, *args, **kwargs): + def send_venue(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_venue(update.effective_chat.id, *args, **kwargs) @@ -407,7 +414,7 @@ class Chat(TelegramObject): """ return self.bot.send_venue(self.id, *args, **kwargs) - def send_video(self, *args, **kwargs): + def send_video(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_video(update.effective_chat.id, *args, **kwargs) @@ -418,7 +425,7 @@ class Chat(TelegramObject): """ return self.bot.send_video(self.id, *args, **kwargs) - def send_video_note(self, *args, **kwargs): + def send_video_note(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_video_note(update.effective_chat.id, *args, **kwargs) @@ -429,7 +436,7 @@ class Chat(TelegramObject): """ return self.bot.send_video_note(self.id, *args, **kwargs) - def send_voice(self, *args, **kwargs): + def send_voice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_voice(update.effective_chat.id, *args, **kwargs) @@ -440,7 +447,7 @@ class Chat(TelegramObject): """ return self.bot.send_voice(self.id, *args, **kwargs) - def send_poll(self, *args, **kwargs): + def send_poll(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_poll(update.effective_chat.id, *args, **kwargs) diff --git a/telegram/chataction.py b/telegram/chataction.py index 0ff4024d8..b8bb3de32 100644 --- a/telegram/chataction.py +++ b/telegram/chataction.py @@ -23,23 +23,23 @@ class ChatAction: """Helper class to provide constants for different chat actions.""" - FIND_LOCATION = 'find_location' + FIND_LOCATION: str = 'find_location' """:obj:`str`: 'find_location'""" - RECORD_AUDIO = 'record_audio' + RECORD_AUDIO: str = 'record_audio' """:obj:`str`: 'record_audio'""" - RECORD_VIDEO = 'record_video' + RECORD_VIDEO: str = 'record_video' """:obj:`str`: 'record_video'""" - RECORD_VIDEO_NOTE = 'record_video_note' + RECORD_VIDEO_NOTE: str = 'record_video_note' """:obj:`str`: 'record_video_note'""" - TYPING = 'typing' + TYPING: str = 'typing' """:obj:`str`: 'typing'""" - UPLOAD_AUDIO = 'upload_audio' + UPLOAD_AUDIO: str = 'upload_audio' """:obj:`str`: 'upload_audio'""" - UPLOAD_DOCUMENT = 'upload_document' + UPLOAD_DOCUMENT: str = 'upload_document' """:obj:`str`: 'upload_document'""" - UPLOAD_PHOTO = 'upload_photo' + UPLOAD_PHOTO: str = 'upload_photo' """:obj:`str`: 'upload_photo'""" - UPLOAD_VIDEO = 'upload_video' + UPLOAD_VIDEO: str = 'upload_video' """:obj:`str`: 'upload_video'""" - UPLOAD_VIDEO_NOTE = 'upload_video_note' + UPLOAD_VIDEO_NOTE: str = 'upload_video_note' """:obj:`str`: 'upload_video_note'""" diff --git a/telegram/chatmember.py b/telegram/chatmember.py index 72f8c53a8..36aba2edc 100644 --- a/telegram/chatmember.py +++ b/telegram/chatmember.py @@ -17,10 +17,16 @@ # 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 an object that represents a Telegram ChatMember.""" +import datetime from telegram import User, TelegramObject from telegram.utils.helpers import to_timestamp, from_timestamp +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot + class ChatMember(TelegramObject): """This object contains information about one member of a chat. @@ -104,26 +110,40 @@ class ChatMember(TelegramObject): may add web page previews to his messages. """ - ADMINISTRATOR = 'administrator' + ADMINISTRATOR: str = 'administrator' """:obj:`str`: 'administrator'""" - CREATOR = 'creator' + CREATOR: str = 'creator' """:obj:`str`: 'creator'""" - KICKED = 'kicked' + KICKED: str = 'kicked' """:obj:`str`: 'kicked'""" - LEFT = 'left' + LEFT: str = 'left' """:obj:`str`: 'left'""" - MEMBER = 'member' + MEMBER: str = 'member' """:obj:`str`: 'member'""" - RESTRICTED = 'restricted' + RESTRICTED: str = 'restricted' """:obj:`str`: 'restricted'""" - def __init__(self, user, status, until_date=None, can_be_edited=None, - can_change_info=None, can_post_messages=None, can_edit_messages=None, - can_delete_messages=None, can_invite_users=None, - can_restrict_members=None, can_pin_messages=None, - can_promote_members=None, can_send_messages=None, - can_send_media_messages=None, can_send_polls=None, can_send_other_messages=None, - can_add_web_page_previews=None, is_member=None, custom_title=None, **kwargs): + def __init__(self, + user: User, + status: str, + until_date: datetime.datetime = None, + can_be_edited: bool = None, + can_change_info: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_invite_users: bool = None, + can_restrict_members: bool = None, + can_pin_messages: bool = None, + can_promote_members: bool = None, + can_send_messages: bool = None, + can_send_media_messages: bool = None, + can_send_polls: bool = None, + can_send_other_messages: bool = None, + can_add_web_page_previews: bool = None, + is_member: bool = None, + custom_title: str = None, + **kwargs: Any): # Required self.user = user self.status = status @@ -148,18 +168,18 @@ class ChatMember(TelegramObject): self._id_attrs = (self.user, self.status) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMember']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['user'] = User.de_json(data.get('user'), bot) data['until_date'] = from_timestamp(data.get('until_date', None)) return cls(**data) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['until_date'] = to_timestamp(self.until_date) diff --git a/telegram/chatpermissions.py b/telegram/chatpermissions.py index 5700bf126..835691e9c 100644 --- a/telegram/chatpermissions.py +++ b/telegram/chatpermissions.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram ChatPermission.""" from telegram import TelegramObject +from typing import Any class ChatPermissions(TelegramObject): @@ -76,9 +77,16 @@ class ChatPermissions(TelegramObject): """ - def __init__(self, can_send_messages=None, can_send_media_messages=None, can_send_polls=None, - can_send_other_messages=None, can_add_web_page_previews=None, - can_change_info=None, can_invite_users=None, can_pin_messages=None, **kwargs): + def __init__(self, + can_send_messages: bool = None, + can_send_media_messages: bool = None, + can_send_polls: bool = None, + can_send_other_messages: bool = None, + can_add_web_page_previews: bool = None, + can_change_info: bool = None, + can_invite_users: bool = None, + can_pin_messages: bool = None, + **kwargs: Any): # Required self.can_send_messages = can_send_messages self.can_send_media_messages = can_send_media_messages @@ -99,10 +107,3 @@ class ChatPermissions(TelegramObject): self.can_invite_users, self.can_pin_messages ) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegram/choseninlineresult.py b/telegram/choseninlineresult.py index 6bcadc9e3..67dcbb0f3 100644 --- a/telegram/choseninlineresult.py +++ b/telegram/choseninlineresult.py @@ -20,6 +20,10 @@ """This module contains an object that represents a Telegram ChosenInlineResult.""" from telegram import TelegramObject, User, Location +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class ChosenInlineResult(TelegramObject): @@ -58,12 +62,12 @@ class ChosenInlineResult(TelegramObject): """ def __init__(self, - result_id, - from_user, - query, - location=None, - inline_message_id=None, - **kwargs): + result_id: str, + from_user: User, + query: str, + location: Location = None, + inline_message_id: str = None, + **kwargs: Any): # Required self.result_id = result_id self.from_user = from_user @@ -75,11 +79,12 @@ class ChosenInlineResult(TelegramObject): self._id_attrs = (self.result_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChosenInlineResult']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) # Required data['from_user'] = User.de_json(data.pop('from'), bot) # Optionals diff --git a/telegram/constants.py b/telegram/constants.py index 0eb4160db..675175497 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -40,18 +40,19 @@ Attributes: formatting styles) """ +from typing import List -MAX_MESSAGE_LENGTH = 4096 -MAX_CAPTION_LENGTH = 1024 +MAX_MESSAGE_LENGTH: int = 4096 +MAX_CAPTION_LENGTH: int = 1024 # constants above this line are tested -SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443] -MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB) -MAX_FILESIZE_UPLOAD = int(50E6) # (50MB) -MAX_PHOTOSIZE_UPLOAD = int(10E6) # (10MB) -MAX_MESSAGES_PER_SECOND_PER_CHAT = 1 -MAX_MESSAGES_PER_SECOND = 30 -MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20 -MAX_MESSAGE_ENTITIES = 100 -MAX_INLINE_QUERY_RESULTS = 50 +SUPPORTED_WEBHOOK_PORTS: List[int] = [443, 80, 88, 8443] +MAX_FILESIZE_DOWNLOAD: int = int(20E6) # (20MB) +MAX_FILESIZE_UPLOAD: int = int(50E6) # (50MB) +MAX_PHOTOSIZE_UPLOAD: int = int(10E6) # (10MB) +MAX_MESSAGES_PER_SECOND_PER_CHAT: int = 1 +MAX_MESSAGES_PER_SECOND: int = 30 +MAX_MESSAGES_PER_MINUTE_PER_GROUP: int = 20 +MAX_MESSAGE_ENTITIES: int = 100 +MAX_INLINE_QUERY_RESULTS: int = 50 diff --git a/telegram/dice.py b/telegram/dice.py index 521333db8..628e34a5e 100644 --- a/telegram/dice.py +++ b/telegram/dice.py @@ -19,6 +19,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Dice.""" from telegram import TelegramObject +from typing import Any, List class Dice(TelegramObject): @@ -47,25 +48,18 @@ class Dice(TelegramObject): value (:obj:`int`): Value of the dice. 1-6 for dice and darts, 1-5 for basketball. emoji (:obj:`str`): Emoji on which the dice throw animation is based. """ - def __init__(self, value, emoji, **kwargs): + def __init__(self, value: int, emoji: str, **kwargs: Any): self.value = value self.emoji = emoji self._id_attrs = (self.value, self.emoji) - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) - - DICE = '🎲' + DICE: str = '🎲' """:obj:`str`: '🎲'""" - DARTS = '🎯' + DARTS: str = '🎯' """:obj:`str`: '🎯'""" BASKETBALL = '🏀' """:obj:`str`: '🏀'""" - ALL_EMOJI = [DICE, DARTS, BASKETBALL] + ALL_EMOJI: List[str] = [DICE, DARTS, BASKETBALL] """List[:obj:`str`]: List of all supported base emoji. Currently :attr:`DICE`, :attr:`DARTS` and :attr:`BASKETBALL`.""" diff --git a/telegram/error.py b/telegram/error.py index dc6b26be7..3ea4da16e 100644 --- a/telegram/error.py +++ b/telegram/error.py @@ -17,9 +17,10 @@ # 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 an object that represents Telegram errors.""" +from typing import Tuple -def _lstrip_str(in_s, lstr): +def _lstrip_str(in_s: str, lstr: str) -> str: """ Args: in_s (:obj:`str`): in string @@ -37,7 +38,7 @@ def _lstrip_str(in_s, lstr): class TelegramError(Exception): - def __init__(self, message): + def __init__(self, message: str): super().__init__() msg = _lstrip_str(message, 'Error: ') @@ -48,10 +49,10 @@ class TelegramError(Exception): msg = msg.capitalize() self.message = msg - def __str__(self): - return '%s' % (self.message) + def __str__(self) -> str: + return '%s' % self.message - def __reduce__(self): + def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self.message,) @@ -60,10 +61,10 @@ class Unauthorized(TelegramError): class InvalidToken(TelegramError): - def __init__(self): + def __init__(self) -> None: super().__init__('Invalid token') - def __reduce__(self): + def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override] return self.__class__, () @@ -76,10 +77,10 @@ class BadRequest(NetworkError): class TimedOut(NetworkError): - def __init__(self): + def __init__(self) -> None: super().__init__('Timed out') - def __reduce__(self): + def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override] return self.__class__, () @@ -90,11 +91,11 @@ class ChatMigrated(TelegramError): """ - def __init__(self, new_chat_id): + def __init__(self, new_chat_id: int): super().__init__('Group migrated to supergroup. New chat id: {}'.format(new_chat_id)) self.new_chat_id = new_chat_id - def __reduce__(self): + def __reduce__(self) -> Tuple[type, Tuple[int]]: # type: ignore[override] return self.__class__, (self.new_chat_id,) @@ -105,11 +106,11 @@ class RetryAfter(TelegramError): """ - def __init__(self, retry_after): + def __init__(self, retry_after: int): super().__init__('Flood control exceeded. Retry in {} seconds'.format(float(retry_after))) self.retry_after = float(retry_after) - def __reduce__(self): + def __reduce__(self) -> Tuple[type, Tuple[float]]: # type: ignore[override] return self.__class__, (self.retry_after,) @@ -122,8 +123,8 @@ class Conflict(TelegramError): """ - def __init__(self, msg): + def __init__(self, msg: str): super().__init__(msg) - def __reduce__(self): + def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self.message,) diff --git a/telegram/ext/basepersistence.py b/telegram/ext/basepersistence.py index b29f0d3d2..841b83576 100644 --- a/telegram/ext/basepersistence.py +++ b/telegram/ext/basepersistence.py @@ -24,6 +24,9 @@ from copy import copy from telegram import Bot +from typing import DefaultDict, Dict, Any, Tuple, Optional, cast +from telegram.utils.types import ConversationDict + class BasePersistence(ABC): """Interface class for adding persistence to your bot. @@ -70,7 +73,7 @@ class BasePersistence(ABC): persistence class. Default is :obj:`True` . """ - def __new__(cls, *args, **kwargs): + def __new__(cls, *args: Any, **kwargs: Any) -> 'BasePersistence': instance = super().__new__(cls) get_user_data = instance.get_user_data get_chat_data = instance.get_chat_data @@ -79,22 +82,22 @@ class BasePersistence(ABC): update_chat_data = instance.update_chat_data update_bot_data = instance.update_bot_data - def get_user_data_insert_bot(): + def get_user_data_insert_bot() -> DefaultDict[int, Dict[Any, Any]]: return instance.insert_bot(get_user_data()) - def get_chat_data_insert_bot(): + def get_chat_data_insert_bot() -> DefaultDict[int, Dict[Any, Any]]: return instance.insert_bot(get_chat_data()) - def get_bot_data_insert_bot(): + def get_bot_data_insert_bot() -> Dict[Any, Any]: return instance.insert_bot(get_bot_data()) - def update_user_data_replace_bot(user_id, data): + def update_user_data_replace_bot(user_id: int, data: Dict) -> None: return update_user_data(user_id, instance.replace_bot(data)) - def update_chat_data_replace_bot(chat_id, data): + def update_chat_data_replace_bot(chat_id: int, data: Dict) -> None: return update_chat_data(chat_id, instance.replace_bot(data)) - def update_bot_data_replace_bot(data): + def update_bot_data_replace_bot(data: Dict) -> None: return update_bot_data(instance.replace_bot(data)) instance.get_user_data = get_user_data_insert_bot @@ -105,13 +108,16 @@ class BasePersistence(ABC): instance.update_bot_data = update_bot_data_replace_bot return instance - def __init__(self, store_user_data=True, store_chat_data=True, store_bot_data=True): + def __init__(self, + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True): self.store_user_data = store_user_data self.store_chat_data = store_chat_data self.store_bot_data = store_bot_data - self.bot = None + self.bot: Bot = None # type: ignore[assignment] - def set_bot(self, bot): + def set_bot(self, bot: Bot) -> None: """Set the Bot to be used by this persistence instance. Args: @@ -120,7 +126,7 @@ class BasePersistence(ABC): self.bot = bot @classmethod - def replace_bot(cls, obj): + def replace_bot(cls, obj: object) -> object: """ Replaces all instances of :class:`telegram.Bot` that occur within the passed object with :attr:`REPLACED_BOT`. Currently, this handles objects of type ``list``, ``tuple``, ``set``, @@ -140,6 +146,7 @@ class BasePersistence(ABC): new_obj = copy(obj) if isinstance(obj, (dict, defaultdict)): + new_obj = cast(dict, new_obj) new_obj.clear() for k, v in obj.items(): new_obj[cls.replace_bot(k)] = cls.replace_bot(v) @@ -156,7 +163,7 @@ class BasePersistence(ABC): return obj - def insert_bot(self, obj): + def insert_bot(self, obj: object) -> object: """ Replaces all instances of :attr:`REPLACED_BOT` that occur within the passed object with :attr:`bot`. Currently, this handles objects of type ``list``, ``tuple``, ``set``, @@ -178,6 +185,7 @@ class BasePersistence(ABC): new_obj = copy(obj) if isinstance(obj, (dict, defaultdict)): + new_obj = cast(dict, new_obj) new_obj.clear() for k, v in obj.items(): new_obj[self.insert_bot(k)] = self.insert_bot(v) @@ -194,7 +202,7 @@ class BasePersistence(ABC): return obj @abstractmethod - def get_user_data(self): + def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]: """"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)``. @@ -204,7 +212,7 @@ class BasePersistence(ABC): """ @abstractmethod - def get_chat_data(self): + def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]: """"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)``. @@ -214,7 +222,7 @@ class BasePersistence(ABC): """ @abstractmethod - def get_bot_data(self): + def get_bot_data(self) -> Dict[Any, Any]: """"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`. @@ -224,7 +232,7 @@ class BasePersistence(ABC): """ @abstractmethod - def get_conversations(self, name): + def get_conversations(self, name: str) -> ConversationDict: """"Will be called by :class:`telegram.ext.Dispatcher` when a :class:`telegram.ext.ConversationHandler` is added if :attr:`telegram.ext.ConversationHandler.persistent` is :obj:`True`. @@ -238,7 +246,9 @@ class BasePersistence(ABC): """ @abstractmethod - def update_conversation(self, name, key, new_state): + def update_conversation(self, + name: str, key: Tuple[int, ...], + new_state: Optional[object]) -> None: """Will be called when a :attr:`telegram.ext.ConversationHandler.update_state` is called. This allows the storage of the new state in the persistence. @@ -249,7 +259,7 @@ class BasePersistence(ABC): """ @abstractmethod - def update_user_data(self, user_id, data): + def update_user_data(self, user_id: int, data: Dict) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. @@ -259,7 +269,7 @@ class BasePersistence(ABC): """ @abstractmethod - def update_chat_data(self, chat_id, data): + def update_chat_data(self, chat_id: int, data: Dict) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. @@ -269,7 +279,7 @@ class BasePersistence(ABC): """ @abstractmethod - def update_bot_data(self, data): + def update_bot_data(self, data: Dict) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. @@ -277,7 +287,7 @@ class BasePersistence(ABC): data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data` . """ - def flush(self): + 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. If this is not of any importance just pass will be sufficient. diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index 682fef87d..16bee0aa7 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -17,8 +17,13 @@ # 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 CallbackContext class.""" +from queue import Queue +from typing import Dict, Any, Tuple, TYPE_CHECKING, Optional, Match, List, NoReturn, Union from telegram import Update +if TYPE_CHECKING: + from telegram import Bot + from telegram.ext import Dispatcher, Job, JobQueue class CallbackContext: @@ -80,7 +85,7 @@ class CallbackContext: """ - def __init__(self, dispatcher): + def __init__(self, dispatcher: 'Dispatcher'): """ Args: dispatcher (:class:`telegram.ext.Dispatcher`): @@ -90,49 +95,54 @@ class CallbackContext: 'dispatcher!') self._dispatcher = dispatcher self._bot_data = dispatcher.bot_data - self._chat_data = None - self._user_data = None - self.args = None - self.matches = None - self.error = None - self.job = None - self.async_args = None - self.async_kwargs = None + self._chat_data: Optional[Dict[Any, Any]] = None + self._user_data: Optional[Dict[Any, Any]] = None + self.args: Optional[List[str]] = None + self.matches: Optional[List[Match]] = None + self.error: Optional[Exception] = None + self.job: Optional['Job'] = None + self.async_args: Optional[Union[List, Tuple]] = None + self.async_kwargs: Optional[Dict[str, Any]] = None @property - def dispatcher(self): + def dispatcher(self) -> 'Dispatcher': """:class:`telegram.ext.Dispatcher`: The dispatcher associated with this context.""" return self._dispatcher @property - def bot_data(self): + def bot_data(self) -> Dict: return self._bot_data @bot_data.setter - def bot_data(self, value): + def bot_data(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to bot_data, see " "https://git.io/fjxKe") @property - def chat_data(self): + def chat_data(self) -> Optional[Dict]: return self._chat_data @chat_data.setter - def chat_data(self, value): + def chat_data(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to chat_data, see " "https://git.io/fjxKe") @property - def user_data(self): + def user_data(self) -> Optional[Dict]: return self._user_data @user_data.setter - def user_data(self, value): + def user_data(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to user_data, see " "https://git.io/fjxKe") @classmethod - def from_error(cls, update, error, dispatcher, async_args=None, async_kwargs=None): + def from_error(cls, + update: object, + error: Exception, + dispatcher: 'Dispatcher', + async_args: Union[List, Tuple] = None, + async_kwargs: Dict[str, Any] = None) -> 'CallbackContext': self = cls.from_update(update, dispatcher) self.error = error self.async_args = async_args @@ -140,7 +150,7 @@ class CallbackContext: return self @classmethod - def from_update(cls, update, dispatcher): + def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackContext': self = cls(dispatcher) if update is not None and isinstance(update, Update): @@ -154,21 +164,21 @@ class CallbackContext: return self @classmethod - def from_job(cls, job, dispatcher): + def from_job(cls, job: 'Job', dispatcher: 'Dispatcher') -> 'CallbackContext': self = cls(dispatcher) self.job = job return self - def update(self, data): + def update(self, data: Dict[str, Any]) -> None: self.__dict__.update(data) @property - def bot(self): + def bot(self) -> 'Bot': """:class:`telegram.Bot`: The bot associated with this context.""" return self._dispatcher.bot @property - def job_queue(self): + def job_queue(self) -> Optional['JobQueue']: """ :class:`telegram.ext.JobQueue`: The ``JobQueue`` used by the :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater` @@ -178,7 +188,7 @@ class CallbackContext: return self._dispatcher.job_queue @property - def update_queue(self): + def update_queue(self) -> Queue: """ :class:`queue.Queue`: The ``Queue`` instance used by the :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater` @@ -188,13 +198,13 @@ class CallbackContext: return self._dispatcher.update_queue @property - def match(self): + def match(self) -> Optional[Match[str]]: """ `Regex match type`: The first match from :attr:`matches`. Useful if you are only filtering using a single regex filter. Returns `None` if :attr:`matches` is empty. """ try: - return self.matches[0] # pylint: disable=unsubscriptable-object + return self.matches[0] # type: ignore[index] # pylint: disable=unsubscriptable-object except (IndexError, TypeError): return None diff --git a/telegram/ext/callbackqueryhandler.py b/telegram/ext/callbackqueryhandler.py index 27180ecc0..f6f07d8f2 100644 --- a/telegram/ext/callbackqueryhandler.py +++ b/telegram/ext/callbackqueryhandler.py @@ -23,6 +23,15 @@ import re from telegram import Update from .handler import Handler +from telegram.utils.types import HandlerArg +from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Pattern, Match, Dict, \ + cast + +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class CallbackQueryHandler(Handler): """Handler class to handle Telegram callback queries. Optionally based on a regex. @@ -102,15 +111,15 @@ class CallbackQueryHandler(Handler): """ def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pattern=None, - pass_groups=False, - pass_groupdict=False, - pass_user_data=False, - pass_chat_data=False, - run_async=False): + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pattern: Union[str, Pattern] = None, + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: bool = False): super().__init__( callback, pass_update_queue=pass_update_queue, @@ -126,7 +135,7 @@ class CallbackQueryHandler(Handler): self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -144,16 +153,26 @@ class CallbackQueryHandler(Handler): return match else: return True + return None - def collect_optional_args(self, dispatcher, update=None, check_result=None): + def collect_optional_args(self, + dispatcher: 'Dispatcher', + update: HandlerArg = None, + check_result: Union[bool, Match] = None) -> Dict[str, Any]: optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pattern: + check_result = cast(Match, check_result) if self.pass_groups: optional_args['groups'] = check_result.groups() if self.pass_groupdict: optional_args['groupdict'] = check_result.groupdict() return optional_args - def collect_additional_context(self, context, update, dispatcher, check_result): + def collect_additional_context(self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Union[bool, Match]) -> None: if self.pattern: + check_result = cast(Match, check_result) context.matches = [check_result] diff --git a/telegram/ext/choseninlineresulthandler.py b/telegram/ext/choseninlineresulthandler.py index fc3af0615..48957c06c 100644 --- a/telegram/ext/choseninlineresulthandler.py +++ b/telegram/ext/choseninlineresulthandler.py @@ -21,6 +21,10 @@ from telegram import Update from .handler import Handler +from telegram.utils.types import HandlerArg +from typing import Optional, Union, TypeVar +RT = TypeVar('RT') + class ChosenInlineResultHandler(Handler): """Handler class to handle Telegram updates that contain a chosen inline result. @@ -80,7 +84,7 @@ class ChosenInlineResultHandler(Handler): """ - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: diff --git a/telegram/ext/commandhandler.py b/telegram/ext/commandhandler.py index b67bb7721..184128eea 100644 --- a/telegram/ext/commandhandler.py +++ b/telegram/ext/commandhandler.py @@ -20,12 +20,19 @@ import re import warnings -from telegram.ext import Filters +from telegram.ext import Filters, BaseFilter from telegram.utils.deprecate import TelegramDeprecationWarning from telegram import Update, MessageEntity from .handler import Handler +from telegram.utils.types import HandlerArg +from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict, List, Tuple +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class CommandHandler(Handler): """Handler class to handle Telegram commands. @@ -124,16 +131,16 @@ class CommandHandler(Handler): """ def __init__(self, - command, - callback, - filters=None, - allow_edited=None, - pass_args=False, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False, - run_async=False): + command: Union[str, List[str]], + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + filters: BaseFilter = None, + allow_edited: bool = None, + pass_args: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: bool = False): super().__init__( callback, pass_update_queue=pass_update_queue, @@ -163,7 +170,10 @@ class CommandHandler(Handler): self.filters &= ~Filters.update.edited_message self.pass_args = pass_args - def check_update(self, update): + def check_update( + self, + update: HandlerArg) -> Optional[Union[bool, Tuple[List[str], + Optional[Union[bool, Dict]]]]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -177,14 +187,14 @@ class CommandHandler(Handler): message = update.effective_message if (message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND - and message.entities[0].offset == 0): + and message.entities[0].offset == 0 and message.text and message.bot): command = message.text[1:message.entities[0].length] args = message.text.split()[1:] - command = command.split('@') - command.append(message.bot.username) + command_parts = command.split('@') + command_parts.append(message.bot.username) - if not (command[0].lower() in self.command - and command[1].lower() == message.bot.username.lower()): + if not (command_parts[0].lower() in self.command + and command_parts[1].lower() == message.bot.username.lower()): return None filter_result = self.filters(update) @@ -192,17 +202,29 @@ class CommandHandler(Handler): return args, filter_result else: return False + return None - def collect_optional_args(self, dispatcher, update=None, check_result=None): + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: HandlerArg = None, + check_result: Optional[Union[bool, Tuple[List[str], + Optional[bool]]]] = None) -> Dict[str, Any]: optional_args = super().collect_optional_args(dispatcher, update) - if self.pass_args: + if self.pass_args and isinstance(check_result, tuple): optional_args['args'] = check_result[0] return optional_args - def collect_additional_context(self, context, update, dispatcher, check_result): - context.args = check_result[0] - if isinstance(check_result[1], dict): - context.update(check_result[1]) + def collect_additional_context( + self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]]) -> None: + if isinstance(check_result, tuple): + context.args = check_result[0] + if isinstance(check_result[1], dict): + context.update(check_result[1]) class PrefixHandler(CommandHandler): @@ -309,20 +331,20 @@ class PrefixHandler(CommandHandler): """ def __init__(self, - prefix, - command, - callback, - filters=None, - pass_args=False, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False, - run_async=False): + prefix: Union[str, List[str]], + command: Union[str, List[str]], + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + filters: BaseFilter = None, + pass_args: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: bool = False): - self._prefix = list() - self._command = list() - self._commands = list() + self._prefix: List[str] = list() + self._command: List[str] = list() + self._commands: List[str] = list() super().__init__( 'nocommand', callback, filters=filters, allow_edited=None, pass_args=pass_args, @@ -332,38 +354,39 @@ class PrefixHandler(CommandHandler): pass_chat_data=pass_chat_data, run_async=run_async) - self.prefix = prefix - self.command = command + self.prefix = prefix # type: ignore[assignment] + self.command = command # type: ignore[assignment] self._build_commands() @property - def prefix(self): + def prefix(self) -> List[str]: return self._prefix @prefix.setter - def prefix(self, prefix): + def prefix(self, prefix: Union[str, List[str]]) -> None: if isinstance(prefix, str): self._prefix = [prefix.lower()] else: self._prefix = prefix self._build_commands() - @property - def command(self): + @property # type: ignore[override] + def command(self) -> List[str]: # type: ignore[override] return self._command @command.setter - def command(self, command): + def command(self, command: Union[str, List[str]]) -> None: if isinstance(command, str): self._command = [command.lower()] else: self._command = command self._build_commands() - def _build_commands(self): + def _build_commands(self) -> None: self._commands = [x.lower() + y.lower() for x in self.prefix for y in self.command] - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[Union[bool, Tuple[List[str], + Optional[Union[bool, Dict]]]]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -385,8 +408,15 @@ class PrefixHandler(CommandHandler): return text_list[1:], filter_result else: return False + return None - def collect_additional_context(self, context, update, dispatcher, check_result): - context.args = check_result[0] - if isinstance(check_result[1], dict): - context.update(check_result[1]) + def collect_additional_context( + self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]]) -> None: + if isinstance(check_result, tuple): + context.args = check_result[0] + if isinstance(check_result[1], dict): + context.update(check_result[1]) diff --git a/telegram/ext/conversationhandler.py b/telegram/ext/conversationhandler.py index 1f3b0dc88..cbb341f62 100644 --- a/telegram/ext/conversationhandler.py +++ b/telegram/ext/conversationhandler.py @@ -24,12 +24,24 @@ from threading import Lock from telegram import Update from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler, - ChosenInlineResultHandler, CallbackContext, DispatcherHandlerStop) + ChosenInlineResultHandler, CallbackContext, BasePersistence, + DispatcherHandlerStop) from telegram.utils.promise import Promise +from telegram.utils.types import ConversationDict, HandlerArg +from typing import Dict, Any, List, Optional, Tuple, TYPE_CHECKING, cast, NoReturn + +if TYPE_CHECKING: + from telegram.ext import Dispatcher, Job +CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]] + class _ConversationTimeoutContext: - def __init__(self, conversation_key, update, dispatcher, callback_context): + def __init__(self, + conversation_key: Tuple[int, ...], + update: Update, + dispatcher: 'Dispatcher', + callback_context: Optional[CallbackContext]): self.conversation_key = conversation_key self.update = update self.dispatcher = dispatcher @@ -157,17 +169,17 @@ class ConversationHandler(Handler): previous ``@run_sync`` decorated running handler to finish.""" def __init__(self, - entry_points, - states, - fallbacks, - allow_reentry=False, - per_chat=True, - per_user=True, - per_message=False, - conversation_timeout=None, - name=None, - persistent=False, - map_to_parent=None): + entry_points: List[Handler], + states: Dict[object, List[Handler]], + fallbacks: List[Handler], + allow_reentry: bool = False, + per_chat: bool = True, + per_user: bool = True, + per_message: bool = False, + conversation_timeout: int = None, + name: str = None, + persistent: bool = False, + map_to_parent: Dict[object, object] = None): self.run_async = False self._entry_points = entry_points @@ -182,15 +194,15 @@ class ConversationHandler(Handler): self._name = name if persistent and not self.name: raise ValueError("Conversations can't be persistent when handler is unnamed.") - self.persistent = persistent - self._persistence = None + self.persistent: bool = persistent + self._persistence: Optional[BasePersistence] = None """:obj:`telegram.ext.BasePersistence`: The persistence used to store conversations. Set by dispatcher""" self._map_to_parent = map_to_parent - self.timeout_jobs = dict() + self.timeout_jobs: Dict[Tuple[int, ...], 'Job'] = dict() self._timeout_jobs_lock = Lock() - self._conversations = dict() + self._conversations: ConversationDict = dict() self._conversations_lock = Lock() self.logger = logging.getLogger(__name__) @@ -231,92 +243,92 @@ class ConversationHandler(Handler): break @property - def entry_points(self): + def entry_points(self) -> List[Handler]: return self._entry_points @entry_points.setter - def entry_points(self, value): + def entry_points(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to entry_points after initialization.') @property - def states(self): + def states(self) -> Dict[object, List[Handler]]: return self._states @states.setter - def states(self, value): + def states(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to states after initialization.') @property - def fallbacks(self): + def fallbacks(self) -> List[Handler]: return self._fallbacks @fallbacks.setter - def fallbacks(self, value): + def fallbacks(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to fallbacks after initialization.') @property - def allow_reentry(self): + def allow_reentry(self) -> bool: return self._allow_reentry @allow_reentry.setter - def allow_reentry(self, value): + def allow_reentry(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to allow_reentry after initialization.') @property - def per_user(self): + def per_user(self) -> bool: return self._per_user @per_user.setter - def per_user(self, value): + def per_user(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to per_user after initialization.') @property - def per_chat(self): + def per_chat(self) -> bool: return self._per_chat @per_chat.setter - def per_chat(self, value): + def per_chat(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to per_chat after initialization.') @property - def per_message(self): + def per_message(self) -> bool: return self._per_message @per_message.setter - def per_message(self, value): + def per_message(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to per_message after initialization.') @property - def conversation_timeout(self): + def conversation_timeout(self) -> Optional[int]: return self._conversation_timeout @conversation_timeout.setter - def conversation_timeout(self, value): + def conversation_timeout(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to conversation_timeout after ' 'initialization.') @property - def name(self): + def name(self) -> Optional[str]: return self._name @name.setter - def name(self, value): + def name(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to name after initialization.') @property - def map_to_parent(self): + def map_to_parent(self) -> Optional[Dict[object, object]]: return self._map_to_parent @map_to_parent.setter - def map_to_parent(self, value): + def map_to_parent(self, value: Any) -> NoReturn: raise ValueError('You can not assign a new value to map_to_parent after initialization.') @property - def persistence(self): + def persistence(self) -> Optional[BasePersistence]: return self._persistence @persistence.setter - def persistence(self, persistence): + def persistence(self, persistence: BasePersistence) -> None: self._persistence = persistence # Set persistence for nested conversations for handlers in self.states.values(): @@ -325,37 +337,37 @@ class ConversationHandler(Handler): handler.persistence = self.persistence @property - def conversations(self): + def conversations(self) -> ConversationDict: return self._conversations @conversations.setter - def conversations(self, value): + def conversations(self, value: ConversationDict) -> None: self._conversations = value # Set conversations for nested conversations for handlers in self.states.values(): for handler in handlers: - if isinstance(handler, ConversationHandler): + if isinstance(handler, ConversationHandler) and self.persistence and handler.name: handler.conversations = self.persistence.get_conversations(handler.name) - def _get_key(self, update): + def _get_key(self, update: Update) -> Tuple[int, ...]: chat = update.effective_chat user = update.effective_user key = list() if self.per_chat: - key.append(chat.id) + key.append(chat.id) # type: ignore[union-attr] if self.per_user and user is not None: key.append(user.id) if self.per_message: - key.append(update.callback_query.inline_message_id - or update.callback_query.message.message_id) + key.append(update.callback_query.inline_message_id # type: ignore[union-attr] + or update.callback_query.message.message_id) # type: ignore[union-attr] return tuple(key) - def check_update(self, update): + def check_update(self, update: HandlerArg) -> CheckUpdateType: """ Determines whether an update should be handled by this conversationhandler, and if so in which state the conversation currently is. @@ -399,11 +411,11 @@ class ConversationHandler(Handler): with self._conversations_lock: state = self.conversations.get(key) else: - handlers = self.states.get(self.WAITING, []) - for handler in handlers: - check = handler.check_update(update) + hdlrs = self.states.get(self.WAITING, []) + for hdlr in hdlrs: + check = hdlr.check_update(update) if check is not None and check is not False: - return key, handler, check + return key, hdlr, check return None self.logger.debug('selecting conversation {} with state {}'.format(str(key), str(state))) @@ -443,9 +455,13 @@ class ConversationHandler(Handler): else: return None - return key, handler, check + return key, handler, check # type: ignore[return-value] - def handle_update(self, update, dispatcher, check_result, context=None): + def handle_update(self, # type: ignore[override] + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: CheckUpdateType, + context: CallbackContext = None) -> Optional[object]: """Send the update to the callback for the current state and Handler Args: @@ -453,9 +469,12 @@ class ConversationHandler(Handler): handler, and the handler's check result. update (:class:`telegram.Update`): Incoming telegram update. dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. + context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by + the dispatcher. """ - conversation_key, handler, check_result = check_result + update = cast(Update, update) # for mypy + conversation_key, handler, check_result = check_result # type: ignore[assignment,misc] raise_dp_handler_stop = False with self._timeout_jobs_lock: @@ -464,18 +483,16 @@ class ConversationHandler(Handler): if timeout_job is not None: timeout_job.schedule_removal() - try: new_state = handler.handle_update(update, dispatcher, check_result, context) except DispatcherHandlerStop as e: new_state = e.state raise_dp_handler_stop = True - with self._timeout_jobs_lock: - if self.conversation_timeout and new_state != self.END: + if self.conversation_timeout and new_state != self.END and dispatcher.job_queue: # Add the new timeout job self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once( - self._trigger_timeout, self.conversation_timeout, + self._trigger_timeout, self.conversation_timeout, # type: ignore[arg-type] context=_ConversationTimeoutContext(conversation_key, update, dispatcher, context)) @@ -491,30 +508,35 @@ class ConversationHandler(Handler): # Don't pass the new state here. If we're in a nested conversation, the parent is # expecting None as return value. raise DispatcherHandlerStop() + return None - def update_state(self, new_state, key): + def update_state(self, + new_state: object, + key: Tuple[int, ...]) -> None: if new_state == self.END: with self._conversations_lock: if key in self.conversations: # If there is no key in conversations, nothing is done. del self.conversations[key] - if self.persistent: + if self.persistent and self.persistence and self.name: self.persistence.update_conversation(self.name, key, None) elif isinstance(new_state, Promise): with self._conversations_lock: self.conversations[key] = (self.conversations.get(key), new_state) - if self.persistent: + if self.persistent and self.persistence and self.name: self.persistence.update_conversation(self.name, key, (self.conversations.get(key), new_state)) elif new_state is not None: with self._conversations_lock: self.conversations[key] = new_state - if self.persistent: + if self.persistent and self.persistence and self.name: self.persistence.update_conversation(self.name, key, new_state) - def _trigger_timeout(self, context, job=None): + def _trigger_timeout(self, + context: _ConversationTimeoutContext, + job: 'Job' = None) -> None: self.logger.debug('conversation timeout was triggered!') # Backward compatibility with bots that do not use CallbackContext @@ -522,7 +544,7 @@ class ConversationHandler(Handler): if isinstance(context, CallbackContext): job = context.job - context = job.context + context = job.context # type:ignore[union-attr,assignment] callback_context = context.callback_context with self._timeout_jobs_lock: diff --git a/telegram/ext/defaults.py b/telegram/ext/defaults.py index 0bdcac18a..3ac8da5dd 100644 --- a/telegram/ext/defaults.py +++ b/telegram/ext/defaults.py @@ -18,8 +18,9 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the class Defaults, which allows to pass default values to Updater.""" import pytz +from typing import Union, Optional, Any, NoReturn -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue class Defaults: @@ -60,14 +61,14 @@ class Defaults: ``pytz`` module. Defaults to UTC. """ def __init__(self, - parse_mode=None, - disable_notification=None, - disable_web_page_preview=None, + parse_mode: str = None, + disable_notification: bool = None, + disable_web_page_preview: bool = None, # Timeout needs special treatment, since the bot methods have two different # default values for timeout (None and 20s) - timeout=DEFAULT_NONE, - quote=None, - tzinfo=pytz.utc): + timeout: Union[float, DefaultValue] = DEFAULT_NONE, + quote: bool = None, + tzinfo: pytz.BaseTzInfo = pytz.utc): self._parse_mode = parse_mode self._disable_notification = disable_notification self._disable_web_page_preview = disable_web_page_preview @@ -76,60 +77,60 @@ class Defaults: self._tzinfo = tzinfo @property - def parse_mode(self): + def parse_mode(self) -> Optional[str]: return self._parse_mode @parse_mode.setter - def parse_mode(self, value): + def parse_mode(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property - def disable_notification(self): + def disable_notification(self) -> Optional[bool]: return self._disable_notification @disable_notification.setter - def disable_notification(self, value): + def disable_notification(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property - def disable_web_page_preview(self): + def disable_web_page_preview(self) -> Optional[bool]: return self._disable_web_page_preview @disable_web_page_preview.setter - def disable_web_page_preview(self, value): + def disable_web_page_preview(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property - def timeout(self): + def timeout(self) -> Union[float, DefaultValue]: return self._timeout @timeout.setter - def timeout(self, value): + def timeout(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property - def quote(self): + def quote(self) -> Optional[bool]: return self._quote @quote.setter - def quote(self, value): + def quote(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property - def tzinfo(self): + def tzinfo(self) -> pytz.BaseTzInfo: return self._tzinfo @tzinfo.setter - def tzinfo(self, value): + def tzinfo(self, value: Any) -> NoReturn: raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") - def __hash__(self): + def __hash__(self) -> int: return hash((self._parse_mode, self._disable_notification, self._disable_web_page_preview, @@ -137,10 +138,10 @@ class Defaults: self._quote, self._tzinfo)) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, Defaults): return self.__dict__ == other.__dict__ return False - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not self == other diff --git a/telegram/ext/dictpersistence.py b/telegram/ext/dictpersistence.py index 21424f3ba..78a69be1c 100644 --- a/telegram/ext/dictpersistence.py +++ b/telegram/ext/dictpersistence.py @@ -25,10 +25,13 @@ from telegram.utils.helpers import decode_user_chat_data_from_json,\ try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] from collections import defaultdict from telegram.ext import BasePersistence +from typing import DefaultDict, Dict, Any, Tuple, Optional +from telegram.utils.types import ConversationDict + class DictPersistence(BasePersistence): """Using python's dicts and json for making your bot persistent. @@ -74,13 +77,13 @@ class DictPersistence(BasePersistence): """ def __init__(self, - store_user_data=True, - store_chat_data=True, - store_bot_data=True, - user_data_json='', - chat_data_json='', - bot_data_json='', - conversations_json=''): + store_user_data: bool = True, + store_chat_data: bool = True, + store_bot_data: bool = True, + user_data_json: str = '', + chat_data_json: str = '', + bot_data_json: str = '', + conversations_json: str = ''): super().__init__(store_user_data=store_user_data, store_chat_data=store_chat_data, store_bot_data=store_bot_data) @@ -121,12 +124,12 @@ class DictPersistence(BasePersistence): raise TypeError("Unable to deserialize conversations_json. Not valid JSON") @property - def user_data(self): + def user_data(self) -> Optional[DefaultDict[int, Dict]]: """:obj:`dict`: The user_data as a dict.""" return self._user_data @property - def user_data_json(self): + def user_data_json(self) -> str: """:obj:`str`: The user_data serialized as a JSON-string.""" if self._user_data_json: return self._user_data_json @@ -134,12 +137,12 @@ class DictPersistence(BasePersistence): return json.dumps(self.user_data) @property - def chat_data(self): + def chat_data(self) -> Optional[DefaultDict[int, Dict]]: """:obj:`dict`: The chat_data as a dict.""" return self._chat_data @property - def chat_data_json(self): + def chat_data_json(self) -> str: """:obj:`str`: The chat_data serialized as a JSON-string.""" if self._chat_data_json: return self._chat_data_json @@ -147,12 +150,12 @@ class DictPersistence(BasePersistence): return json.dumps(self.chat_data) @property - def bot_data(self): + def bot_data(self) -> Optional[Dict]: """:obj:`dict`: The bot_data as a dict.""" return self._bot_data @property - def bot_data_json(self): + def bot_data_json(self) -> str: """:obj:`str`: The bot_data serialized as a JSON-string.""" if self._bot_data_json: return self._bot_data_json @@ -160,19 +163,19 @@ class DictPersistence(BasePersistence): return json.dumps(self.bot_data) @property - def conversations(self): + def conversations(self) -> Optional[Dict[str, Dict[Tuple, Any]]]: """:obj:`dict`: The conversations as a dict.""" return self._conversations @property - def conversations_json(self): + def conversations_json(self) -> str: """:obj:`str`: The conversations serialized as a JSON-string.""" if self._conversations_json: return self._conversations_json else: - return encode_conversations_to_json(self.conversations) + return encode_conversations_to_json(self.conversations) # type: ignore[arg-type] - def get_user_data(self): + def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]: """Returns the user_data created from the ``user_data_json`` or an empty :obj:`defaultdict`. @@ -183,9 +186,9 @@ class DictPersistence(BasePersistence): pass else: self._user_data = defaultdict(dict) - return deepcopy(self.user_data) + return deepcopy(self.user_data) # type: ignore[arg-type] - def get_chat_data(self): + def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]: """Returns the chat_data created from the ``chat_data_json`` or an empty :obj:`defaultdict`. @@ -196,9 +199,9 @@ class DictPersistence(BasePersistence): pass else: self._chat_data = defaultdict(dict) - return deepcopy(self.chat_data) + return deepcopy(self.chat_data) # type: ignore[arg-type] - def get_bot_data(self): + def get_bot_data(self) -> Dict[Any, Any]: """Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`. Returns: @@ -208,9 +211,9 @@ class DictPersistence(BasePersistence): pass else: self._bot_data = {} - return deepcopy(self.bot_data) + return deepcopy(self.bot_data) # type: ignore[arg-type] - def get_conversations(self, name): + def get_conversations(self, name: str) -> ConversationDict: """Returns the conversations created from the ``conversations_json`` or an empty :obj:`dict`. @@ -221,9 +224,11 @@ class DictPersistence(BasePersistence): pass else: self._conversations = {} - return self.conversations.get(name, {}).copy() + return self.conversations.get(name, {}).copy() # type: ignore[union-attr] - def update_conversation(self, name, key, new_state): + def update_conversation(self, + name: str, key: Tuple[int, ...], + new_state: Optional[object]) -> None: """Will update the conversations for the given handler. Args: @@ -231,12 +236,14 @@ class DictPersistence(BasePersistence): key (:obj:`tuple`): The key the state is changed for. new_state (:obj:`tuple` | :obj:`any`): The new state for the given key. """ + if not self._conversations: + self._conversations = {} if self._conversations.setdefault(name, {}).get(key) == new_state: return self._conversations[name][key] = new_state self._conversations_json = None - def update_user_data(self, user_id, data): + def update_user_data(self, user_id: int, data: Dict) -> None: """Will update the user_data (if changed). Args: @@ -250,7 +257,7 @@ class DictPersistence(BasePersistence): self._user_data[user_id] = data self._user_data_json = None - def update_chat_data(self, chat_id, data): + def update_chat_data(self, chat_id: int, data: Dict) -> None: """Will update the chat_data (if changed). Args: @@ -264,7 +271,7 @@ class DictPersistence(BasePersistence): self._chat_data[chat_id] = data self._chat_data_json = None - def update_bot_data(self, data): + def update_bot_data(self, data: Dict) -> None: """Will update the bot_data (if changed). Args: diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 5f2f8f85f..fcf47b1a3 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -36,10 +36,19 @@ from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.promise import Promise from telegram.ext import BasePersistence +from typing import Any, Callable, TYPE_CHECKING, Optional, Union, DefaultDict, Dict, List, Set + +from telegram.utils.types import HandlerArg + +if TYPE_CHECKING: + from telegram import Bot + from telegram.ext import JobQueue + DEFAULT_GROUP = 0 -def run_async(func): +def run_async(func: Callable[[Update, CallbackContext], + Any]) -> Callable[[Update, CallbackContext], Any]: """ Function decorator that will run the function in a new thread. @@ -57,7 +66,7 @@ def run_async(func): """ @wraps(func) - def async_func(*args, **kwargs): + def async_func(*args: Any, **kwargs: Any) -> Any: warnings.warn('The @run_async decorator is deprecated. Use the `run_async` parameter of' '`Dispatcher.add_handler` or `Dispatcher.run_async` instead.', TelegramDeprecationWarning, @@ -87,7 +96,7 @@ class DispatcherHandlerStop(Exception): Args: state (:obj:`object`, optional): The next state of the conversation. """ - def __init__(self, state=None): + def __init__(self, state: object = None) -> None: super().__init__() self.state = state @@ -129,13 +138,13 @@ class Dispatcher: logger = logging.getLogger(__name__) def __init__(self, - bot, - update_queue, - workers=4, - exception_event=None, - job_queue=None, - persistence=None, - use_context=True): + bot: 'Bot', + update_queue: Queue, + workers: int = 4, + exception_event: Event = None, + job_queue: 'JobQueue' = None, + persistence: BasePersistence = None, + use_context: bool = True): self.bot = bot self.update_queue = update_queue self.job_queue = job_queue @@ -146,9 +155,10 @@ class Dispatcher: warnings.warn('Old Handler API is deprecated - see https://git.io/fxJuV for details', TelegramDeprecationWarning, stacklevel=3) - self.user_data = defaultdict(dict) - self.chat_data = defaultdict(dict) + self.user_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict) + self.chat_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict) self.bot_data = {} + self.persistence: Optional[BasePersistence] = None self._update_persistence_lock = Lock() if persistence: if not isinstance(persistence, BasePersistence): @@ -170,11 +180,11 @@ class Dispatcher: else: self.persistence = None - self.handlers = {} + self.handlers: Dict[int, List[Handler]] = {} """Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group.""" - self.groups = [] + self.groups: List[int] = [] """List[:obj:`int`]: A list with all groups.""" - self.error_handlers = {} + self.error_handlers: Dict[Callable, bool] = {} """Dict[:obj:`callable`, :obj:`bool`]: A dict, where the keys are error handlers and the values indicate whether they are to be run asynchronously.""" @@ -182,22 +192,22 @@ class Dispatcher: """:obj:`bool`: Indicates if this dispatcher is running.""" self.__stop_event = Event() self.__exception_event = exception_event or Event() - self.__async_queue = Queue() - self.__async_threads = set() + self.__async_queue: Queue = Queue() + self.__async_threads: Set[Thread] = set() # For backward compatibility, we allow a "singleton" mode for the dispatcher. When there's # only one instance of Dispatcher, it will be possible to use the `run_async` decorator. with self.__singleton_lock: - if self.__singleton_semaphore.acquire(blocking=0): + if self.__singleton_semaphore.acquire(blocking=False): self._set_singleton(self) else: self._set_singleton(None) @property - def exception_event(self): + def exception_event(self) -> Event: return self.__exception_event - def _init_async_threads(self, base_name, workers): + def _init_async_threads(self, base_name: str, workers: int) -> None: base_name = '{}_'.format(base_name) if base_name else '' for i in range(workers): @@ -207,12 +217,12 @@ class Dispatcher: thread.start() @classmethod - def _set_singleton(cls, val): + def _set_singleton(cls, val: Optional['Dispatcher']) -> None: cls.logger.debug('Setting singleton dispatcher as %s', val) cls.__singleton = weakref.ref(val) if val else None @classmethod - def get_instance(cls): + def get_instance(cls) -> 'Dispatcher': """Get the singleton instance of this class. Returns: @@ -223,12 +233,12 @@ class Dispatcher: """ if cls.__singleton is not None: - return cls.__singleton() # pylint: disable=not-callable + return cls.__singleton() # type: ignore[return-value] # pylint: disable=not-callable else: raise RuntimeError('{} not initialized or multiple instances exist'.format( cls.__name__)) - def _pooled(self): + def _pooled(self) -> None: thr_name = current_thread().getName() while 1: promise = self.__async_queue.get() @@ -270,7 +280,11 @@ class Dispatcher: except Exception: self.logger.exception('An uncaught error was raised while handling the error.') - def run_async(self, func, *args, update=None, **kwargs): + def run_async(self, + func: Callable[..., Any], + *args: Any, + update: HandlerArg = None, + **kwargs: Any) -> Promise: """ Queue a function (with given args/kwargs) to be run asynchronously. Exceptions raised by the function will be handled by the error handlers registered with @@ -296,13 +310,18 @@ class Dispatcher: """ return self._run_async(func, *args, update=update, error_handling=True, **kwargs) - def _run_async(self, func, *args, update=None, error_handling=True, **kwargs): + def _run_async(self, + func: Callable[..., Any], + *args: Any, + update: HandlerArg = None, + error_handling: bool = True, + **kwargs: Any) -> Promise: # TODO: Remove error_handling parameter once we drop the @run_async decorator promise = Promise(func, args, kwargs, update=update, error_handling=error_handling) self.__async_queue.put(promise) return promise - def start(self, ready=None): + def start(self, ready: Event = None) -> None: """Thread target of thread 'dispatcher'. Runs in background and processes the update queue. @@ -323,7 +342,7 @@ class Dispatcher: self.logger.error(msg) raise TelegramError(msg) - self._init_async_threads(uuid4(), self.workers) + self._init_async_threads(str(uuid4()), self.workers) self.running = True self.logger.debug('Dispatcher started') @@ -350,7 +369,7 @@ class Dispatcher: self.running = False self.logger.debug('Dispatcher thread stopped') - def stop(self): + def stop(self) -> None: """Stops the thread.""" if self.running: self.__stop_event.set() @@ -374,10 +393,10 @@ class Dispatcher: self.logger.debug('async thread {}/{} has ended'.format(i + 1, total)) @property - def has_running_threads(self): + def has_running_threads(self) -> bool: return self.running or bool(self.__async_threads) - def process_update(self, update): + def process_update(self, update: Union[str, Update, TelegramError]) -> None: """Processes a single update. Args: @@ -427,7 +446,7 @@ class Dispatcher: except Exception: self.logger.exception('An uncaught error was raised while handling the error.') - def add_handler(self, handler, group=DEFAULT_GROUP): + def add_handler(self, handler: Handler, 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 @@ -459,7 +478,7 @@ class Dispatcher: raise TypeError('handler is not an instance of {}'.format(Handler.__name__)) if not isinstance(group, int): raise TypeError('group is not int') - if isinstance(handler, ConversationHandler) and handler.persistent: + if isinstance(handler, ConversationHandler) and handler.persistent and handler.name: if not self.persistence: raise ValueError( "ConversationHandler {} can not be persistent if dispatcher has no " @@ -474,7 +493,7 @@ class Dispatcher: self.handlers[group].append(handler) - def remove_handler(self, handler, group=DEFAULT_GROUP): + def remove_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None: """Remove a handler from the specified group. Args: @@ -488,7 +507,7 @@ class Dispatcher: del self.handlers[group] self.groups.remove(group) - def update_persistence(self, update=None): + def update_persistence(self, update: HandlerArg = None) -> None: """Update :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`. Args: @@ -498,7 +517,7 @@ class Dispatcher: with self._update_persistence_lock: self.__update_persistence(update) - def __update_persistence(self, update): + def __update_persistence(self, update: HandlerArg = None) -> None: if self.persistence: # We use list() here in order to decouple chat_ids from self.chat_data, as dict view # objects will change, when the dict does and we want to loop over chat_ids @@ -551,7 +570,9 @@ class Dispatcher: 'the error with an error_handler' self.logger.exception(message) - def add_error_handler(self, callback, run_async=False): + def add_error_handler(self, + callback: Callable[[Any, CallbackContext], None], + run_async: bool = False) -> None: """Registers an error handler in the Dispatcher. This handler will receive every error which happens in your bot. @@ -580,7 +601,7 @@ class Dispatcher: return self.error_handlers[callback] = run_async - def remove_error_handler(self, callback): + def remove_error_handler(self, callback: Callable[[Any, CallbackContext], None]) -> None: """Removes an error handler. Args: @@ -589,7 +610,10 @@ class Dispatcher: """ self.error_handlers.pop(callback, None) - def dispatch_error(self, update, error, promise=None): + def dispatch_error(self, + update: Optional[HandlerArg], + error: Exception, + promise: Promise = None) -> None: """Dispatches an error. Args: diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 964448904..ee0d2309c 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -23,7 +23,9 @@ import re from abc import ABC, abstractmethod from threading import Lock -from telegram import Chat, Update, MessageEntity +from telegram import Chat, Update, MessageEntity, Message + +from typing import Optional, Dict, Union, List, Pattern, Match, cast, Set, FrozenSet __all__ = ['Filters', 'BaseFilter', 'MessageFilter', 'UpdateFilter', 'InvertedFilter', 'MergedFilter'] @@ -85,19 +87,19 @@ class BaseFilter(ABC): data_filter = False @abstractmethod - def __call__(self, update): + def __call__(self, update: Update) -> Optional[Union[bool, Dict]]: pass - def __and__(self, other): + def __and__(self, other: 'BaseFilter') -> 'BaseFilter': return MergedFilter(self, and_filter=other) - def __or__(self, other): + def __or__(self, other: 'BaseFilter') -> 'BaseFilter': return MergedFilter(self, or_filter=other) - def __invert__(self): + def __invert__(self) -> 'BaseFilter': return InvertedFilter(self) - def __repr__(self): + def __repr__(self) -> str: # We do this here instead of in a __init__ so filter don't have to call __init__ or super() if self.name is None: self.name = self.__class__.__name__ @@ -118,11 +120,11 @@ class MessageFilter(BaseFilter, ABC): (depends on the handler). """ - def __call__(self, update): + def __call__(self, update: Update) -> Optional[Union[bool, Dict]]: return self.filter(update.effective_message) @abstractmethod - def filter(self, message): + def filter(self, message: Message) -> Optional[Union[bool, Dict]]: """This method must be overwritten. Args: @@ -149,11 +151,12 @@ class UpdateFilter(BaseFilter, ABC): (depends on the handler). """ - def __call__(self, update): + def __call__(self, update: Update) -> Optional[Union[bool, Dict]]: return self.filter(update) @abstractmethod - def filter(self, update): + def filter(self, + update: Update) -> Optional[Union[bool, Dict]]: """This method must be overwritten. Args: @@ -172,13 +175,13 @@ class InvertedFilter(UpdateFilter): f: The filter to invert. """ - def __init__(self, f): + def __init__(self, f: BaseFilter): self.f = f - def filter(self, update): + def filter(self, update: Update) -> bool: return not bool(self.f(update)) - def __repr__(self): + def __repr__(self) -> str: return "".format(self.f) @@ -191,7 +194,10 @@ class MergedFilter(UpdateFilter): or_filter: Optional filter to "or" with base_filter. Mutually exclusive with and_filter. """ - def __init__(self, base_filter, and_filter=None, or_filter=None): + def __init__(self, + base_filter: BaseFilter, + and_filter: BaseFilter = None, + or_filter: BaseFilter = None): self.base_filter = base_filter if self.base_filter.data_filter: self.data_filter = True @@ -206,7 +212,7 @@ class MergedFilter(UpdateFilter): and self.or_filter.data_filter): self.data_filter = True - def _merge(self, base_output, comp_output): + def _merge(self, base_output: Union[bool, Dict], comp_output: Union[bool, Dict]) -> Dict: base = base_output if isinstance(base_output, dict) else {} comp = comp_output if isinstance(comp_output, dict) else {} for k in comp.keys(): @@ -222,7 +228,7 @@ class MergedFilter(UpdateFilter): base[k] = comp_value return base - def filter(self, update): + def filter(self, update: Update) -> Union[bool, Dict]: base_output = self.base_filter(update) # We need to check if the filters are data filters and if so return the merged data. # If it's not a data filter or an or_filter but no matches return bool @@ -250,41 +256,44 @@ class MergedFilter(UpdateFilter): return True return False - def __repr__(self): + def __repr__(self) -> str: return "<{} {} {}>".format(self.base_filter, "and" if self.and_filter else "or", self.and_filter or self.or_filter) class _DiceEmoji(MessageFilter): - def __init__(self, emoji=None, name=None): + def __init__(self, emoji: str = None, name: str = None): self.name = 'Filters.dice.{}'.format(name) if name else 'Filters.dice' self.emoji = emoji class _DiceValues(MessageFilter): - def __init__(self, values, name, emoji=None): + def __init__(self, values: Union[int, List[int]], name: str, emoji: str = None): self.values = [values] if isinstance(values, int) else values self.emoji = emoji self.name = '{}({})'.format(name, values) - def filter(self, message): - if bool(message.dice and message.dice.value in self.values): + def filter(self, message: Message) -> bool: + if message.dice and message.dice.value in self.values: if self.emoji: return message.dice.emoji == self.emoji return True + return False - def __call__(self, update): + def __call__(self, # type: ignore[override] + update: Union[Update, List[int]]) -> Union[bool, '_DiceValues']: if isinstance(update, Update): return self.filter(update.effective_message) else: return self._DiceValues(update, self.name, emoji=self.emoji) - def filter(self, message): + def filter(self, message: Message) -> bool: if bool(message.dice): if self.emoji: return message.dice.emoji == self.emoji return True + return False class Filters: @@ -300,7 +309,7 @@ class Filters: class _All(MessageFilter): name = 'Filters.all' - def filter(self, message): + def filter(self, message: Message) -> bool: return True all = _All() @@ -311,22 +320,23 @@ class Filters: class _TextStrings(MessageFilter): - def __init__(self, strings): + def __init__(self, strings: List[str]): self.strings = strings self.name = 'Filters.text({})'.format(strings) - def filter(self, message): + def filter(self, message: Message) -> bool: if message.text: return message.text in self.strings return False - def __call__(self, update): + def __call__(self, # type: ignore[override] + update: Union[Update, List[str]]) -> Union[bool, '_TextStrings']: if isinstance(update, Update): return self.filter(update.effective_message) else: return self._TextStrings(update) - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.text) text = _Text() @@ -362,22 +372,23 @@ class Filters: class _CaptionStrings(MessageFilter): - def __init__(self, strings): + def __init__(self, strings: List[str]): self.strings = strings self.name = 'Filters.caption({})'.format(strings) - def filter(self, message): + def filter(self, message: Message) -> bool: if message.caption: return message.caption in self.strings return False - def __call__(self, update): + def __call__(self, # type: ignore[override] + update: Union[Update, List[str]]) -> Union[bool, '_CaptionStrings']: if isinstance(update, Update): return self.filter(update.effective_message) else: return self._CaptionStrings(update) - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.caption) caption = _Caption() @@ -397,23 +408,25 @@ class Filters: class _CommandOnlyStart(MessageFilter): - def __init__(self, only_start): + def __init__(self, only_start: bool): self.only_start = only_start self.name = 'Filters.command({})'.format(only_start) - def filter(self, message): - return (message.entities - and any([e.type == MessageEntity.BOT_COMMAND for e in message.entities])) + def filter(self, message: Message) -> bool: + return bool(message.entities + and any([e.type == MessageEntity.BOT_COMMAND + for e in message.entities])) - def __call__(self, update): + def __call__(self, # type: ignore[override] + update: Union[bool, Update]) -> Union[bool, '_CommandOnlyStart']: if isinstance(update, Update): return self.filter(update.effective_message) else: return self._CommandOnlyStart(update) - def filter(self, message): - return (message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND - and message.entities[0].offset == 0) + def filter(self, message: Message) -> bool: + return bool(message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND + and message.entities[0].offset == 0) command = _Command() """ @@ -465,24 +478,26 @@ class Filters: data_filter = True - def __init__(self, pattern): + def __init__(self, pattern: Union[str, Pattern]): if isinstance(pattern, str): pattern = re.compile(pattern) - self.pattern = pattern + pattern = cast(Pattern, pattern) + self.pattern: Pattern = pattern self.name = 'Filters.regex({})'.format(self.pattern) - def filter(self, message): + def filter(self, + message: Message) -> Optional[Dict[str, List[Match]]]: """""" # remove method from docs if message.text: match = self.pattern.search(message.text) if match: return {'matches': [match]} - return {} + return {} class _Reply(MessageFilter): name = 'Filters.reply' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.reply_to_message) reply = _Reply() @@ -491,7 +506,7 @@ class Filters: class _Audio(MessageFilter): name = 'Filters.audio' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.audio) audio = _Audio() @@ -514,7 +529,7 @@ class Filters: of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. """ - def __init__(self, category): + def __init__(self, category: Optional[str]): """Initialize the category you want to filter Args: @@ -522,10 +537,11 @@ class Filters: self.category = category self.name = "Filters.document.category('{}')".format(self.category) - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs if message.document: return message.document.mime_type.startswith(self.category) + return False application = category('application/') audio = category('audio/') @@ -546,18 +562,19 @@ class Filters: ``Filters.documents.mime_type('audio/mpeg')`` filters all audio in mp3 format. """ - def __init__(self, mimetype): + def __init__(self, mimetype: Optional[str]): """Initialize the category you want to filter Args: - filetype (str, optional): mime_type of the media you want to filter""" + mimetype (str, optional): mime_type of the media you want to filter""" self.mimetype = mimetype self.name = "Filters.document.mime_type('{}')".format(self.mimetype) - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs if message.document: return message.document.mime_type == self.mimetype + return False apk = mime_type('application/vnd.android.package-archive') doc = mime_type('application/msword') @@ -575,7 +592,7 @@ class Filters: xml = mime_type('application/xml') zip = mime_type('application/zip') - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.document) document = _Document() @@ -636,7 +653,7 @@ officedocument.wordprocessingml.document")``- class _Animation(MessageFilter): name = 'Filters.animation' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.animation) animation = _Animation() @@ -645,7 +662,7 @@ officedocument.wordprocessingml.document")``- class _Photo(MessageFilter): name = 'Filters.photo' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.photo) photo = _Photo() @@ -654,7 +671,7 @@ officedocument.wordprocessingml.document")``- class _Sticker(MessageFilter): name = 'Filters.sticker' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.sticker) sticker = _Sticker() @@ -663,7 +680,7 @@ officedocument.wordprocessingml.document")``- class _Video(MessageFilter): name = 'Filters.video' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.video) video = _Video() @@ -672,7 +689,7 @@ officedocument.wordprocessingml.document")``- class _Voice(MessageFilter): name = 'Filters.voice' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.voice) voice = _Voice() @@ -681,7 +698,7 @@ officedocument.wordprocessingml.document")``- class _VideoNote(MessageFilter): name = 'Filters.video_note' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.video_note) video_note = _VideoNote() @@ -690,7 +707,7 @@ officedocument.wordprocessingml.document")``- class _Contact(MessageFilter): name = 'Filters.contact' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.contact) contact = _Contact() @@ -699,7 +716,7 @@ officedocument.wordprocessingml.document")``- class _Location(MessageFilter): name = 'Filters.location' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.location) location = _Location() @@ -708,7 +725,7 @@ officedocument.wordprocessingml.document")``- class _Venue(MessageFilter): name = 'Filters.venue' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.venue) venue = _Venue() @@ -725,7 +742,7 @@ officedocument.wordprocessingml.document")``- class _NewChatMembers(MessageFilter): name = 'Filters.status_update.new_chat_members' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.new_chat_members) new_chat_members = _NewChatMembers() @@ -734,7 +751,7 @@ officedocument.wordprocessingml.document")``- class _LeftChatMember(MessageFilter): name = 'Filters.status_update.left_chat_member' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.left_chat_member) left_chat_member = _LeftChatMember() @@ -743,7 +760,7 @@ officedocument.wordprocessingml.document")``- class _NewChatTitle(MessageFilter): name = 'Filters.status_update.new_chat_title' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.new_chat_title) new_chat_title = _NewChatTitle() @@ -752,7 +769,7 @@ officedocument.wordprocessingml.document")``- class _NewChatPhoto(MessageFilter): name = 'Filters.status_update.new_chat_photo' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.new_chat_photo) new_chat_photo = _NewChatPhoto() @@ -761,7 +778,7 @@ officedocument.wordprocessingml.document")``- class _DeleteChatPhoto(MessageFilter): name = 'Filters.status_update.delete_chat_photo' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.delete_chat_photo) delete_chat_photo = _DeleteChatPhoto() @@ -770,7 +787,7 @@ officedocument.wordprocessingml.document")``- class _ChatCreated(MessageFilter): name = 'Filters.status_update.chat_created' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.group_chat_created or message.supergroup_chat_created or message.channel_chat_created) @@ -782,7 +799,7 @@ officedocument.wordprocessingml.document")``- class _Migrate(MessageFilter): name = 'Filters.status_update.migrate' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.migrate_from_chat_id or message.migrate_to_chat_id) migrate = _Migrate() @@ -792,7 +809,7 @@ officedocument.wordprocessingml.document")``- class _PinnedMessage(MessageFilter): name = 'Filters.status_update.pinned_message' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.pinned_message) pinned_message = _PinnedMessage() @@ -801,7 +818,7 @@ officedocument.wordprocessingml.document")``- class _ConnectedWebsite(MessageFilter): name = 'Filters.status_update.connected_website' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.connected_website) connected_website = _ConnectedWebsite() @@ -809,7 +826,7 @@ officedocument.wordprocessingml.document")``- name = 'Filters.status_update' - def filter(self, message): + def filter(self, message: Update) -> bool: return bool(self.new_chat_members(message) or self.left_chat_member(message) or self.new_chat_title(message) or self.new_chat_photo(message) or self.delete_chat_photo(message) or self.chat_created(message) @@ -848,7 +865,7 @@ officedocument.wordprocessingml.document")``- class _Forwarded(MessageFilter): name = 'Filters.forwarded' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.forward_date) forwarded = _Forwarded() @@ -857,7 +874,7 @@ officedocument.wordprocessingml.document")``- class _Game(MessageFilter): name = 'Filters.game' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.game) game = _Game() @@ -877,11 +894,11 @@ officedocument.wordprocessingml.document")``- """ - def __init__(self, entity_type): + def __init__(self, entity_type: str): self.entity_type = entity_type self.name = 'Filters.entity({})'.format(self.entity_type) - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.entities) @@ -899,18 +916,18 @@ officedocument.wordprocessingml.document")``- """ - def __init__(self, entity_type): + def __init__(self, entity_type: str): self.entity_type = entity_type self.name = 'Filters.caption_entity({})'.format(self.entity_type) - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.caption_entities) class _Private(MessageFilter): name = 'Filters.private' - def filter(self, message): + def filter(self, message: Message) -> bool: return message.chat.type == Chat.PRIVATE private = _Private() @@ -919,7 +936,7 @@ officedocument.wordprocessingml.document")``- class _Group(MessageFilter): name = 'Filters.group' - def filter(self, message): + def filter(self, message: Message) -> bool: return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP] group = _Group() @@ -959,18 +976,21 @@ officedocument.wordprocessingml.document")``- RuntimeError: If user_id and username are both present. """ - def __init__(self, user_id=None, username=None, allow_empty=False): + def __init__(self, + user_id: Union[int, List[int]] = None, + username: Union[str, List[str]] = None, + allow_empty: bool = False): self.allow_empty = allow_empty self.__lock = Lock() - self._user_ids = set() - self._usernames = set() + self._user_ids: Set[int] = set() + self._usernames: Set[str] = set() self._set_user_ids(user_id) self._set_usernames(username) @staticmethod - def _parse_user_id(user_id): + def _parse_user_id(user_id: Union[int, List[int]]) -> Set[int]: if user_id is None: return set() if isinstance(user_id, int): @@ -978,21 +998,21 @@ officedocument.wordprocessingml.document")``- return set(user_id) @staticmethod - def _parse_username(username): + def _parse_username(username: Union[str, List[str]]) -> Set[str]: if username is None: return set() if isinstance(username, str): return {username[1:] if username.startswith('@') else username} return {user[1:] if user.startswith('@') else user for user in username} - def _set_user_ids(self, user_id): + def _set_user_ids(self, user_id: Union[int, List[int]]) -> None: with self.__lock: if user_id and self._usernames: raise RuntimeError("Can't set user_id in conjunction with (already set) " "usernames.") self._user_ids = self._parse_user_id(user_id) - def _set_usernames(self, username): + def _set_usernames(self, username: Union[str, List[str]]) -> None: with self.__lock: if username and self._user_ids: raise RuntimeError("Can't set username in conjunction with (already set) " @@ -1000,24 +1020,24 @@ officedocument.wordprocessingml.document")``- self._usernames = self._parse_username(username) @property - def user_ids(self): + def user_ids(self) -> FrozenSet[int]: with self.__lock: return frozenset(self._user_ids) @user_ids.setter - def user_ids(self, user_id): + def user_ids(self, user_id: Union[int, List[int]]) -> None: self._set_user_ids(user_id) @property - def usernames(self): + def usernames(self) -> FrozenSet[str]: with self.__lock: return frozenset(self._usernames) @usernames.setter - def usernames(self, username): + def usernames(self, username: Union[str, List[str]]) -> None: self._set_usernames(username) - def add_usernames(self, username): + def add_usernames(self, username: Union[str, List[str]]) -> None: """ Add one or more users to the allowed usernames. @@ -1030,10 +1050,10 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set username in conjunction with (already set) " "user_ids.") - username = self._parse_username(username) - self._usernames |= username + parsed_username = self._parse_username(username) + self._usernames |= parsed_username - def add_user_ids(self, user_id): + def add_user_ids(self, user_id: Union[int, List[int]]) -> None: """ Add one or more users to the allowed user ids. @@ -1046,11 +1066,11 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set user_id in conjunction with (already set) " "usernames.") - user_id = self._parse_user_id(user_id) + parsed_user_id = self._parse_user_id(user_id) - self._user_ids |= user_id + self._user_ids |= parsed_user_id - def remove_usernames(self, username): + def remove_usernames(self, username: Union[str, List[str]]) -> None: """ Remove one or more users from allowed usernames. @@ -1063,10 +1083,10 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set username in conjunction with (already set) " "user_ids.") - username = self._parse_username(username) - self._usernames -= username + parsed_username = self._parse_username(username) + self._usernames -= parsed_username - def remove_user_ids(self, user_id): + def remove_user_ids(self, user_id: Union[int, List[int]]) -> None: """ Remove one or more users from allowed user ids. @@ -1078,17 +1098,17 @@ officedocument.wordprocessingml.document")``- if self._usernames: raise RuntimeError("Can't set user_id in conjunction with (already set) " "usernames.") - user_id = self._parse_user_id(user_id) - self._user_ids -= user_id + parsed_user_id = self._parse_user_id(user_id) + self._user_ids -= parsed_user_id - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs if message.from_user: if self.user_ids: return message.from_user.id in self.user_ids if self.usernames: - return (message.from_user.username - and message.from_user.username in self.usernames) + return bool(message.from_user.username + and message.from_user.username in self.usernames) return self.allow_empty return False @@ -1125,19 +1145,21 @@ officedocument.wordprocessingml.document")``- Raises: RuntimeError: If bot_id and username are both present. """ - - def __init__(self, bot_id=None, username=None, allow_empty=False): + def __init__(self, + bot_id: Union[int, List[int]] = None, + username: Union[str, List[str]] = None, + allow_empty: bool = False): self.allow_empty = allow_empty self.__lock = Lock() - self._bot_ids = set() - self._usernames = set() + self._bot_ids: Set[int] = set() + self._usernames: Set[str] = set() self._set_bot_ids(bot_id) self._set_usernames(username) @staticmethod - def _parse_bot_id(bot_id): + def _parse_bot_id(bot_id: Union[int, List[int]]) -> Set[int]: if bot_id is None: return set() if isinstance(bot_id, int): @@ -1145,21 +1167,21 @@ officedocument.wordprocessingml.document")``- return set(bot_id) @staticmethod - def _parse_username(username): + def _parse_username(username: Union[str, List[str]]) -> Set[str]: if username is None: return set() if isinstance(username, str): return {username[1:] if username.startswith('@') else username} return {bot[1:] if bot.startswith('@') else bot for bot in username} - def _set_bot_ids(self, bot_id): + def _set_bot_ids(self, bot_id: Union[int, List[int]]) -> None: with self.__lock: if bot_id and self._usernames: raise RuntimeError("Can't set bot_id in conjunction with (already set) " "usernames.") self._bot_ids = self._parse_bot_id(bot_id) - def _set_usernames(self, username): + def _set_usernames(self, username: Union[str, List[str]]) -> None: with self.__lock: if username and self._bot_ids: raise RuntimeError("Can't set username in conjunction with (already set) " @@ -1167,24 +1189,24 @@ officedocument.wordprocessingml.document")``- self._usernames = self._parse_username(username) @property - def bot_ids(self): + def bot_ids(self) -> FrozenSet[int]: with self.__lock: return frozenset(self._bot_ids) @bot_ids.setter - def bot_ids(self, bot_id): + def bot_ids(self, bot_id: Union[int, List[int]]) -> None: self._set_bot_ids(bot_id) @property - def usernames(self): + def usernames(self) -> FrozenSet[str]: with self.__lock: return frozenset(self._usernames) @usernames.setter - def usernames(self, username): + def usernames(self, username: Union[str, List[str]]) -> None: self._set_usernames(username) - def add_usernames(self, username): + def add_usernames(self, username: Union[str, List[str]]) -> None: """ Add one or more users to the allowed usernames. @@ -1197,11 +1219,12 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set username in conjunction with (already set) " "bot_ids.") - username = self._parse_username(username) - self._usernames |= username + parsed_username = self._parse_username(username) + self._usernames |= parsed_username - def add_bot_ids(self, bot_id): + def add_bot_ids(self, bot_id: Union[int, List[int]]) -> None: """ + Add one or more users to the allowed user ids. Args: @@ -1213,11 +1236,11 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set bot_id in conjunction with (already set) " "usernames.") - bot_id = self._parse_bot_id(bot_id) + parsed_bot_id = self._parse_bot_id(bot_id) - self._bot_ids |= bot_id + self._bot_ids |= parsed_bot_id - def remove_usernames(self, username): + def remove_usernames(self, username: Union[str, List[str]]) -> None: """ Remove one or more users from allowed usernames. @@ -1230,10 +1253,10 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set username in conjunction with (already set) " "bot_ids.") - username = self._parse_username(username) - self._usernames -= username + parsed_username = self._parse_username(username) + self._usernames -= parsed_username - def remove_bot_ids(self, bot_id): + def remove_bot_ids(self, bot_id: Union[int, List[int]]) -> None: """ Remove one or more users from allowed user ids. @@ -1245,17 +1268,17 @@ officedocument.wordprocessingml.document")``- if self._usernames: raise RuntimeError("Can't set bot_id in conjunction with (already set) " "usernames.") - bot_id = self._parse_bot_id(bot_id) - self._bot_ids -= bot_id + parsed_bot_id = self._parse_bot_id(bot_id) + self._bot_ids -= parsed_bot_id - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs if message.via_bot: if self.bot_ids: return message.via_bot.id in self.bot_ids if self.usernames: - return (message.via_bot.username - and message.via_bot.username in self.usernames) + return bool(message.via_bot.username + and message.via_bot.username in self.usernames) return self.allow_empty return False @@ -1293,18 +1316,21 @@ officedocument.wordprocessingml.document")``- """ - def __init__(self, chat_id=None, username=None, allow_empty=False): + def __init__(self, + chat_id: Union[int, List[int]] = None, + username: Union[str, List[str]] = None, + allow_empty: bool = False): self.allow_empty = allow_empty self.__lock = Lock() - self._chat_ids = set() - self._usernames = set() + self._chat_ids: Set[int] = set() + self._usernames: Set[str] = set() self._set_chat_ids(chat_id) self._set_usernames(username) @staticmethod - def _parse_chat_id(chat_id): + def _parse_chat_id(chat_id: Union[int, List[int]]) -> Set[int]: if chat_id is None: return set() if isinstance(chat_id, int): @@ -1312,21 +1338,21 @@ officedocument.wordprocessingml.document")``- return set(chat_id) @staticmethod - def _parse_username(username): + def _parse_username(username: Union[str, List[str]]) -> Set[str]: if username is None: return set() if isinstance(username, str): return {username[1:] if username.startswith('@') else username} return {chat[1:] if chat.startswith('@') else chat for chat in username} - def _set_chat_ids(self, chat_id): + def _set_chat_ids(self, chat_id: Union[int, List[int]]) -> None: with self.__lock: if chat_id and self._usernames: raise RuntimeError("Can't set chat_id in conjunction with (already set) " "usernames.") self._chat_ids = self._parse_chat_id(chat_id) - def _set_usernames(self, username): + def _set_usernames(self, username: Union[str, List[str]]) -> None: with self.__lock: if username and self._chat_ids: raise RuntimeError("Can't set username in conjunction with (already set) " @@ -1334,24 +1360,24 @@ officedocument.wordprocessingml.document")``- self._usernames = self._parse_username(username) @property - def chat_ids(self): + def chat_ids(self) -> FrozenSet[int]: with self.__lock: return frozenset(self._chat_ids) @chat_ids.setter - def chat_ids(self, chat_id): + def chat_ids(self, chat_id: Union[int, List[int]]) -> None: self._set_chat_ids(chat_id) @property - def usernames(self): + def usernames(self) -> FrozenSet[str]: with self.__lock: return frozenset(self._usernames) @usernames.setter - def usernames(self, username): + def usernames(self, username: Union[str, List[str]]) -> None: self._set_usernames(username) - def add_usernames(self, username): + def add_usernames(self, username: Union[str, List[str]]) -> None: """ Add one or more chats to the allowed usernames. @@ -1364,10 +1390,10 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set username in conjunction with (already set) " "chat_ids.") - username = self._parse_username(username) - self._usernames |= username + parsed_username = self._parse_username(username) + self._usernames |= parsed_username - def add_chat_ids(self, chat_id): + def add_chat_ids(self, chat_id: Union[int, List[int]]) -> None: """ Add one or more chats to the allowed chat ids. @@ -1380,11 +1406,11 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set chat_id in conjunction with (already set) " "usernames.") - chat_id = self._parse_chat_id(chat_id) + parsed_chat_id = self._parse_chat_id(chat_id) - self._chat_ids |= chat_id + self._chat_ids |= parsed_chat_id - def remove_usernames(self, username): + def remove_usernames(self, username: Union[str, List[str]]) -> None: """ Remove one or more chats from allowed usernames. @@ -1397,10 +1423,10 @@ officedocument.wordprocessingml.document")``- raise RuntimeError("Can't set username in conjunction with (already set) " "chat_ids.") - username = self._parse_username(username) - self._usernames -= username + parsed_username = self._parse_username(username) + self._usernames -= parsed_username - def remove_chat_ids(self, chat_id): + def remove_chat_ids(self, chat_id: Union[int, List[int]]) -> None: """ Remove one or more chats from allowed chat ids. @@ -1412,24 +1438,24 @@ officedocument.wordprocessingml.document")``- if self._usernames: raise RuntimeError("Can't set chat_id in conjunction with (already set) " "usernames.") - chat_id = self._parse_chat_id(chat_id) - self._chat_ids -= chat_id + parsed_chat_id = self._parse_chat_id(chat_id) + self._chat_ids -= parsed_chat_id - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs if message.chat: if self.chat_ids: return message.chat.id in self.chat_ids if self.usernames: - return (message.chat.username - and message.chat.username in self.usernames) + return bool(message.chat.username + and message.chat.username in self.usernames) return self.allow_empty return False class _Invoice(MessageFilter): name = 'Filters.invoice' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.invoice) invoice = _Invoice() @@ -1438,7 +1464,7 @@ officedocument.wordprocessingml.document")``- class _SuccessfulPayment(MessageFilter): name = 'Filters.successful_payment' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.successful_payment) successful_payment = _SuccessfulPayment() @@ -1447,7 +1473,7 @@ officedocument.wordprocessingml.document")``- class _PassportData(MessageFilter): name = 'Filters.passport_data' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.passport_data) passport_data = _PassportData() @@ -1456,7 +1482,7 @@ officedocument.wordprocessingml.document")``- class _Poll(MessageFilter): name = 'Filters.poll' - def filter(self, message): + def filter(self, message: Message) -> bool: return bool(message.poll) poll = _Poll() @@ -1513,17 +1539,19 @@ officedocument.wordprocessingml.document")``- """ - def __init__(self, lang): + def __init__(self, lang: Union[str, List[str]]): if isinstance(lang, str): + lang = cast(str, lang) self.lang = [lang] else: + lang = cast(List[str], lang) self.lang = lang self.name = 'Filters.language({})'.format(self.lang) - def filter(self, message): + def filter(self, message: Message) -> bool: """""" # remove method from docs - return message.from_user.language_code and any( - [message.from_user.language_code.startswith(x) for x in self.lang]) + return bool(message.from_user.language_code and any( + [message.from_user.language_code.startswith(x) for x in self.lang])) class _UpdateType(UpdateFilter): name = 'Filters.update' @@ -1531,7 +1559,7 @@ officedocument.wordprocessingml.document")``- class _Message(UpdateFilter): name = 'Filters.update.message' - def filter(self, update): + def filter(self, update: Update) -> bool: return update.message is not None message = _Message() @@ -1539,7 +1567,7 @@ officedocument.wordprocessingml.document")``- class _EditedMessage(UpdateFilter): name = 'Filters.update.edited_message' - def filter(self, update): + def filter(self, update: Update) -> bool: return update.edited_message is not None edited_message = _EditedMessage() @@ -1547,7 +1575,7 @@ officedocument.wordprocessingml.document")``- class _Messages(UpdateFilter): name = 'Filters.update.messages' - def filter(self, update): + def filter(self, update: Update) -> bool: return update.message is not None or update.edited_message is not None messages = _Messages() @@ -1555,7 +1583,7 @@ officedocument.wordprocessingml.document")``- class _ChannelPost(UpdateFilter): name = 'Filters.update.channel_post' - def filter(self, update): + def filter(self, update: Update) -> bool: return update.channel_post is not None channel_post = _ChannelPost() @@ -1563,7 +1591,7 @@ officedocument.wordprocessingml.document")``- class _EditedChannelPost(UpdateFilter): name = 'Filters.update.edited_channel_post' - def filter(self, update): + def filter(self, update: Update) -> bool: return update.edited_channel_post is not None edited_channel_post = _EditedChannelPost() @@ -1571,13 +1599,13 @@ officedocument.wordprocessingml.document")``- class _ChannelPosts(UpdateFilter): name = 'Filters.update.channel_posts' - def filter(self, update): + def filter(self, update: Update) -> bool: return update.channel_post is not None or update.edited_channel_post is not None channel_posts = _ChannelPosts() - def filter(self, update): - return self.messages(update) or self.channel_posts(update) + def filter(self, update: Update) -> bool: + return bool(self.messages(update) or self.channel_posts(update)) update = _UpdateType() """Subset for filtering the type of update. diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index 9b1ffe560..7fd2bb589 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -20,6 +20,15 @@ from abc import ABC, abstractmethod +from telegram.utils.promise import Promise +from telegram.utils.types import HandlerArg +from telegram import Update +from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class Handler(ABC): """The base class for all update handlers. Create custom handlers by inheriting from it. @@ -78,15 +87,14 @@ class Handler(ABC): Defaults to :obj:`False`. """ - def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False, - run_async=False): - self.callback = callback + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: bool = False): + self.callback: Callable[[HandlerArg, 'CallbackContext'], RT] = callback self.pass_update_queue = pass_update_queue self.pass_job_queue = pass_job_queue self.pass_user_data = pass_user_data @@ -94,7 +102,7 @@ class Handler(ABC): self.run_async = run_async @abstractmethod - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[Union[bool, object]]: """ This method is called to determine if an update should be handled by this handler instance. It should always be overridden. @@ -109,7 +117,11 @@ class Handler(ABC): """ - def handle_update(self, update, dispatcher, check_result, context=None): + def handle_update(self, + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: object, + context: 'CallbackContext' = None) -> Union[RT, Promise]: """ This method is called if it was determined that an update should indeed be handled by this instance. Calls :attr:`callback` along with its respectful @@ -120,7 +132,9 @@ class Handler(ABC): Args: update (:obj:`str` | :class:`telegram.Update`): The update to be handled. dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher. - check_result: The result from :attr:`check_update`. + check_result (:obj:`obj`): The result from :attr:`check_update`. + context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by + the dispatcher. """ if context: @@ -135,9 +149,13 @@ class Handler(ABC): return dispatcher.run_async(self.callback, dispatcher.bot, update, update=update, **optional_args) else: - return self.callback(dispatcher.bot, update, **optional_args) + return self.callback(dispatcher.bot, update, **optional_args) # type: ignore - def collect_additional_context(self, context, update, dispatcher, check_result): + def collect_additional_context(self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Any) -> None: """Prepares additional arguments for the context. Override if needed. Args: @@ -149,7 +167,10 @@ class Handler(ABC): """ pass - def collect_optional_args(self, dispatcher, update=None, check_result=None): + def collect_optional_args(self, + dispatcher: 'Dispatcher', + update: HandlerArg = None, + check_result: Any = None) -> Dict[str, Any]: """ Prepares the optional arguments. If the handler has additional optional args, it should subclass this method, but remember to call this super method. @@ -163,17 +184,19 @@ class Handler(ABC): check_result: The result from check_update """ - optional_args = dict() + optional_args: Dict[str, Any] = dict() if self.pass_update_queue: optional_args['update_queue'] = dispatcher.update_queue if self.pass_job_queue: optional_args['job_queue'] = dispatcher.job_queue - if self.pass_user_data: + 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] - if self.pass_chat_data: + 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 bbed825bf..2a49addf2 100644 --- a/telegram/ext/inlinequeryhandler.py +++ b/telegram/ext/inlinequeryhandler.py @@ -23,6 +23,15 @@ from telegram import Update from .handler import Handler +from telegram.utils.types import HandlerArg +from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict, Pattern, Match, \ + cast + +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class InlineQueryHandler(Handler): """ @@ -102,22 +111,22 @@ class InlineQueryHandler(Handler): """ def __init__(self, - callback, - pass_update_queue=False, - pass_job_queue=False, - pattern=None, - pass_groups=False, - pass_groupdict=False, - pass_user_data=False, - pass_chat_data=False, - run_async=False): + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pattern: Union[str, Pattern] = None, + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: bool = False): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, - run_async=False) + run_async=run_async) if isinstance(pattern, str): pattern = re.compile(pattern) @@ -126,7 +135,7 @@ class InlineQueryHandler(Handler): self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[Union[bool, Match]]: """ Determines whether an update should be passed to this handlers :attr:`callback`. @@ -146,16 +155,26 @@ class InlineQueryHandler(Handler): return match else: return True + return None - def collect_optional_args(self, dispatcher, update=None, check_result=None): + def collect_optional_args(self, + dispatcher: 'Dispatcher', + update: HandlerArg = None, + check_result: Optional[Union[bool, Match]] = None) -> Dict[str, Any]: optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pattern: + check_result = cast(Match, check_result) if self.pass_groups: optional_args['groups'] = check_result.groups() if self.pass_groupdict: optional_args['groupdict'] = check_result.groupdict() return optional_args - def collect_additional_context(self, context, update, dispatcher, check_result): + def collect_additional_context(self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Optional[Union[bool, Match]]) -> None: if self.pattern: + check_result = cast(Match, check_result) context.matches = [check_result] diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index d8332bb3e..908917711 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -25,10 +25,16 @@ import pytz from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.combining import OrTrigger -from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR +from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR, JobEvent from telegram.ext.callbackcontext import CallbackContext +from typing import TYPE_CHECKING, Union, Callable, Tuple, Optional, List, Any, cast, overload +from telegram.utils.types import JSONDict +if TYPE_CHECKING: + from telegram.ext import Dispatcher + from telegram import Bot + class Days: MON, TUE, WED, THU, FRI, SAT, SUN = range(7) @@ -46,32 +52,32 @@ class JobQueue: """ - def __init__(self): - self._dispatcher = None + def __init__(self) -> None: + self._dispatcher: 'Dispatcher' = None # type: ignore[assignment] self.logger = logging.getLogger(self.__class__.__name__) self.scheduler = BackgroundScheduler(timezone=pytz.utc) self.scheduler.add_listener(self._update_persistence, mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) # Dispatch errors and don't log them in the APS logger - def aps_log_filter(record): + def aps_log_filter(record): # type: ignore return 'raised an exception' not in record.msg logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter) self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR) - def _build_args(self, job): + 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.bot, job] - def _tz_now(self): + def _tz_now(self) -> datetime.datetime: return datetime.datetime.now(self.scheduler.timezone) - def _update_persistence(self, event): + def _update_persistence(self, event: JobEvent) -> None: self._dispatcher.update_persistence() - def _dispatch_error(self, event): + def _dispatch_error(self, event: JobEvent) -> None: try: self._dispatcher.dispatch_error(None, event.exception) # Errors should not stop the thread. @@ -80,7 +86,21 @@ class JobQueue: 'uncaught error was raised while handling the error ' 'with an error_handler.') - def _parse_time_input(self, time, shift_day=False): + @overload + def _parse_time_input(self, time: None, shift_day: bool = False) -> None: + ... + + @overload + def _parse_time_input(self, + time: Union[float, int, datetime.timedelta, datetime.datetime, + datetime.time], + shift_day: bool = False) -> datetime.datetime: + ... + + def _parse_time_input(self, + time: Union[float, int, datetime.timedelta, datetime.datetime, + datetime.time, None], + shift_day: bool = False) -> Optional[datetime.datetime]: if time is None: return None if isinstance(time, (int, float)): @@ -98,7 +118,7 @@ class JobQueue: # isinstance(time, datetime.datetime): return time - def set_dispatcher(self, dispatcher): + def set_dispatcher(self, dispatcher: 'Dispatcher') -> None: """Set the dispatcher to be used by this JobQueue. Use this instead of passing a :class:`telegram.Bot` to the JobQueue, which is deprecated. @@ -111,7 +131,12 @@ class JobQueue: if dispatcher.bot.defaults: self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc) - def run_once(self, callback, when, context=None, name=None, job_kwargs=None): + def run_once(self, + callback: Callable[['CallbackContext'], None], + when: Union[float, datetime.timedelta, datetime.datetime, datetime.time], + context: object = None, + name: str = None, + job_kwargs: JSONDict = None) -> 'Job': """Creates a new ``Job`` that runs once and adds it to the queue. Args: @@ -169,8 +194,16 @@ class JobQueue: job.job = j return job - def run_repeating(self, callback, interval, first=None, last=None, context=None, name=None, - job_kwargs=None): + def run_repeating(self, + callback: Callable[['CallbackContext'], None], + interval: Union[float, datetime.timedelta], + first: Union[float, datetime.timedelta, datetime.datetime, + datetime.time] = None, + last: Union[float, datetime.timedelta, datetime.datetime, + datetime.time] = None, + context: object = None, + name: str = None, + job_kwargs: JSONDict = None) -> 'Job': """Creates a new ``Job`` that runs at specified intervals and adds it to the queue. Args: @@ -256,8 +289,14 @@ class JobQueue: job.job = j return job - def run_monthly(self, callback, when, day, context=None, name=None, day_is_strict=True, - job_kwargs=None): + def run_monthly(self, + callback: Callable[['CallbackContext'], None], + when: datetime.time, + day: int, + context: object = None, + name: str = None, + day_is_strict: bool = True, + job_kwargs: JSONDict = None) -> 'Job': """Creates a new ``Job`` that runs on a monthly basis and adds it to the queue. Args: @@ -325,8 +364,13 @@ class JobQueue: job.job = j return job - def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None, - job_kwargs=None): + def run_daily(self, + callback: Callable[['CallbackContext'], None], + time: datetime.time, + days: Tuple[int, ...] = Days.EVERY_DAY, + context: object = None, + name: str = None, + job_kwargs: JSONDict = None) -> 'Job': """Creates a new ``Job`` that runs on a daily basis and adds it to the queue. Args: @@ -379,7 +423,11 @@ class JobQueue: job.job = j return job - def run_custom(self, callback, job_kwargs, context=None, name=None): + def run_custom(self, + callback: Callable[['CallbackContext'], None], + job_kwargs: JSONDict, + context: object = None, + name: str = None) -> 'Job': """Creates a new customly defined ``Job``. Args: @@ -393,7 +441,7 @@ class JobQueue: job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for ``scheduler.add_job``. context (:obj:`object`, optional): Additional data needed for the callback function. - Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. + Can be accessed through ``job.context`` in the callback. Defaults to ``None``. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. @@ -413,21 +461,21 @@ class JobQueue: job.job = j return job - def start(self): + def start(self) -> None: """Starts the job_queue thread.""" if not self.scheduler.running: self.scheduler.start() - def stop(self): + def stop(self) -> None: """Stops the thread.""" if self.scheduler.running: self.scheduler.shutdown() - def jobs(self): + def jobs(self) -> Tuple['Job', ...]: """Returns a tuple of all jobs that are currently in the ``JobQueue``.""" return tuple(Job.from_aps_job(job, self) for job in self.scheduler.get_jobs()) - def get_jobs_by_name(self, name): + def get_jobs_by_name(self, name: str) -> Tuple['Job', ...]: """Returns a tuple of jobs with the given name that are currently in the ``JobQueue``""" return tuple(job for job in self.jobs() if job.name == name) @@ -469,11 +517,11 @@ class Job: """ def __init__(self, - callback, - context=None, - name=None, - job_queue=None, - job=None): + callback: Callable[['CallbackContext'], None], + context: object = None, + name: str = None, + job_queue: JobQueue = None, + job: 'Job' = None): self.callback = callback self.context = context @@ -483,15 +531,15 @@ class Job: self._removed = False self._enabled = False - self.job = job + self.job = cast('Job', job) - def run(self, dispatcher): + 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)) else: - self.callback(dispatcher.bot, self) + self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg] except Exception as e: try: dispatcher.dispatch_error(None, e) @@ -501,7 +549,7 @@ class Job: 'uncaught error was raised while handling the error ' 'with an error_handler.') - def schedule_removal(self): + def schedule_removal(self) -> None: """ Schedules this job for removal from the ``JobQueue``. It will be removed without executing its callback function again. @@ -510,17 +558,17 @@ class Job: self._removed = True @property - def removed(self): + def removed(self) -> bool: """:obj:`bool`: Whether this job is due to be removed.""" return self._removed @property - def enabled(self): + def enabled(self) -> bool: """:obj:`bool`: Whether this job is enabled.""" return self._enabled @enabled.setter - def enabled(self, status): + def enabled(self, status: bool) -> None: if status: self.job.resume() else: @@ -528,7 +576,7 @@ class Job: self._enabled = status @property - def next_t(self): + def next_t(self) -> Optional[datetime.datetime]: """ :obj:`datetime.datetime`: Datetime for the next job execution. Datetime is localized according to :attr:`tzinfo`. @@ -537,7 +585,7 @@ class Job: return self.job.next_run_time @classmethod - def from_aps_job(cls, job, job_queue): + def from_aps_job(cls, job: 'Job', job_queue: JobQueue) -> 'Job': # context based callbacks if len(job.args) == 1: context = job.args[0].job.context @@ -545,13 +593,13 @@ class Job: context = job.args[1].context return cls(job.func, context=context, name=job.name, job_queue=job_queue, job=job) - def __getattr__(self, item): + def __getattr__(self, item: str) -> Any: return getattr(self.job, item) - def __lt__(self, other): + def __lt__(self, other: object) -> bool: return False - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): return self.id == other.id return False diff --git a/telegram/ext/messagehandler.py b/telegram/ext/messagehandler.py index b6b413ebe..e89daa237 100644 --- a/telegram/ext/messagehandler.py +++ b/telegram/ext/messagehandler.py @@ -23,9 +23,16 @@ import warnings from telegram.utils.deprecate import TelegramDeprecationWarning from telegram import Update -from telegram.ext import Filters +from telegram.ext import Filters, BaseFilter from .handler import Handler +from telegram.utils.types import HandlerArg +from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class MessageHandler(Handler): """Handler class to handle telegram messages. They might contain text, media or status updates. @@ -114,16 +121,16 @@ class MessageHandler(Handler): """ def __init__(self, - filters, - callback, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False, - message_updates=None, - channel_post_updates=None, - edited_updates=None, - run_async=False): + filters: BaseFilter, + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + message_updates: bool = None, + channel_post_updates: bool = None, + edited_updates: bool = None, + run_async: bool = False): super().__init__( callback, @@ -162,7 +169,7 @@ class MessageHandler(Handler): self.filters &= ~(Filters.update.edited_message | Filters.update.edited_channel_post) - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[Union[bool, Dict[str, Any]]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -174,7 +181,12 @@ class MessageHandler(Handler): """ if isinstance(update, Update) and update.effective_message: return self.filters(update) + return None - def collect_additional_context(self, context, update, dispatcher, check_result): + def collect_additional_context(self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Optional[Union[bool, Dict[str, Any]]]) -> None: if isinstance(check_result, dict): context.update(check_result) diff --git a/telegram/ext/messagequeue.py b/telegram/ext/messagequeue.py index abdb93822..6274bab23 100644 --- a/telegram/ext/messagequeue.py +++ b/telegram/ext/messagequeue.py @@ -27,6 +27,14 @@ import time import threading import queue as q +from typing import Callable, Any, TYPE_CHECKING, List, NoReturn + +if TYPE_CHECKING: + from telegram import Bot + +# We need to count < 1s intervals, so the most accurate timer is needed +curtime = time.perf_counter + class DelayQueueError(RuntimeError): """Indicates processing errors.""" @@ -68,12 +76,12 @@ class DelayQueue(threading.Thread): _instcnt = 0 # instance counter def __init__(self, - queue=None, - burst_limit=30, - time_limit_ms=1000, - exc_route=None, - autostart=True, - name=None): + queue: q.Queue = None, + burst_limit: int = 30, + time_limit_ms: int = 1000, + exc_route: Callable[[Exception], None] = None, + autostart: bool = True, + name: str = None): self._queue = queue if queue is not None else q.Queue() self.burst_limit = burst_limit self.time_limit = time_limit_ms / 1000 @@ -87,14 +95,14 @@ class DelayQueue(threading.Thread): if autostart: # immediately start processing super().start() - def run(self): + def run(self) -> None: """ Do not use the method except for unthreaded testing purposes, the method normally is automatically called by autostart argument. """ - times = [] # used to store each callable processing time + times: List[float] = [] # used to store each callable processing time while True: item = self._queue.get() if self.__exit_req: @@ -119,7 +127,7 @@ class DelayQueue(threading.Thread): except Exception as exc: # re-route any exceptions self.exc_route(exc) # to prevent thread exit - def stop(self, timeout=None): + def stop(self, timeout: float = None) -> None: """Used to gently stop processor and shutdown its thread. Args: @@ -136,7 +144,7 @@ class DelayQueue(threading.Thread): super().join(timeout=timeout) @staticmethod - def _default_exception_handler(exc): + def _default_exception_handler(exc: Exception) -> NoReturn: """ Dummy exception handler which re-raises exception in thread. Could be possibly overwritten by subclasses. @@ -145,7 +153,7 @@ class DelayQueue(threading.Thread): raise exc - def __call__(self, func, *args, **kwargs): + def __call__(self, func: Callable, *args: Any, **kwargs: Any) -> None: """Used to process callbacks in throughput-limiting thread through queue. Args: @@ -194,12 +202,12 @@ class MessageQueue: """ def __init__(self, - all_burst_limit=30, - all_time_limit_ms=1000, - group_burst_limit=20, - group_time_limit_ms=60000, - exc_route=None, - autostart=True): + all_burst_limit: int = 30, + all_time_limit_ms: int = 1000, + group_burst_limit: int = 20, + group_time_limit_ms: int = 60000, + exc_route: Callable[[Exception], None] = None, + autostart: bool = True): # create according delay queues, use composition self._all_delayq = DelayQueue( burst_limit=all_burst_limit, @@ -212,18 +220,18 @@ class MessageQueue: exc_route=exc_route, autostart=autostart) - def start(self): + def start(self) -> None: """Method is used to manually start the ``MessageQueue`` processing.""" self._all_delayq.start() self._group_delayq.start() - def stop(self, timeout=None): + def stop(self, timeout: float = None) -> None: self._group_delayq.stop(timeout=timeout) self._all_delayq.stop(timeout=timeout) stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docstring if any - def __call__(self, promise, is_group_msg=False): + def __call__(self, promise: Callable, is_group_msg: bool = False) -> Callable: """ Processes callables in throughput-limiting queues to avoid hitting limits (specified with :attr:`burst_limit` and :attr:`time_limit`. @@ -255,7 +263,7 @@ class MessageQueue: return promise -def queuedmessage(method): +def queuedmessage(method: Callable) -> Callable: """A decorator to be used with :attr:`telegram.Bot` send* methods. Note: @@ -288,12 +296,13 @@ def queuedmessage(method): """ @functools.wraps(method) - def wrapped(self, *args, **kwargs): - queued = kwargs.pop('queued', self._is_messages_queued_default) + def wrapped(self: 'Bot', *args: Any, **kwargs: Any) -> Any: + queued = kwargs.pop('queued', + self._is_messages_queued_default) # type: ignore[attr-defined] isgroup = kwargs.pop('isgroup', False) if queued: prom = promise.Promise(method, (self, ) + args, kwargs) - return self._msg_queue(prom, isgroup) + return self._msg_queue(prom, isgroup) # type: ignore[attr-defined] return method(self, *args, **kwargs) return wrapped diff --git a/telegram/ext/picklepersistence.py b/telegram/ext/picklepersistence.py index 85788dc3c..1e93fb142 100644 --- a/telegram/ext/picklepersistence.py +++ b/telegram/ext/picklepersistence.py @@ -23,6 +23,9 @@ from copy import deepcopy from telegram.ext import BasePersistence +from typing import DefaultDict, Dict, Any, Tuple, Optional +from telegram.utils.types import ConversationDict + class PicklePersistence(BasePersistence): """Using python's builtin pickle for making you bot persistent. @@ -71,24 +74,25 @@ class PicklePersistence(BasePersistence): Default is :obj:`False`. """ - def __init__(self, filename, - store_user_data=True, - store_chat_data=True, - store_bot_data=True, - single_file=True, - on_flush=False): + def __init__(self, + 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): super().__init__(store_user_data=store_user_data, store_chat_data=store_chat_data, store_bot_data=store_bot_data) self.filename = filename self.single_file = single_file self.on_flush = on_flush - self.user_data = None - self.chat_data = None - self.bot_data = None - self.conversations = None + self.user_data: Optional[DefaultDict[int, Dict]] = None + self.chat_data: Optional[DefaultDict[int, Dict]] = None + self.bot_data: Optional[Dict] = None + self.conversations: Optional[Dict[str, Dict[Tuple, Any]]] = None - def load_singlefile(self): + def load_singlefile(self) -> None: try: filename = self.filename with open(self.filename, "rb") as f: @@ -99,7 +103,7 @@ class PicklePersistence(BasePersistence): self.bot_data = data.get('bot_data', {}) self.conversations = data['conversations'] except IOError: - self.conversations = {} + self.conversations = dict() self.user_data = defaultdict(dict) self.chat_data = defaultdict(dict) self.bot_data = {} @@ -108,7 +112,7 @@ class PicklePersistence(BasePersistence): except Exception: raise TypeError("Something went wrong unpickling {}".format(filename)) - def load_file(self, filename): + def load_file(self, filename: str) -> Any: try: with open(filename, "rb") as f: return pickle.load(f) @@ -119,17 +123,17 @@ class PicklePersistence(BasePersistence): except Exception: raise TypeError("Something went wrong unpickling {}".format(filename)) - def dump_singlefile(self): + def dump_singlefile(self) -> None: with open(self.filename, "wb") as f: data = {'conversations': self.conversations, 'user_data': self.user_data, 'chat_data': self.chat_data, 'bot_data': self.bot_data} pickle.dump(data, f) - def dump_file(self, filename, data): + def dump_file(self, filename: str, data: Any) -> None: with open(filename, "wb") as f: pickle.dump(data, f) - def get_user_data(self): + def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]: """Returns the user_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: @@ -147,9 +151,9 @@ class PicklePersistence(BasePersistence): self.user_data = data else: self.load_singlefile() - return deepcopy(self.user_data) + return deepcopy(self.user_data) # type: ignore[arg-type] - def get_chat_data(self): + def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]: """Returns the chat_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: @@ -167,9 +171,9 @@ class PicklePersistence(BasePersistence): self.chat_data = data else: self.load_singlefile() - return deepcopy(self.chat_data) + return deepcopy(self.chat_data) # type: ignore[arg-type] - def get_bot_data(self): + def get_bot_data(self) -> Dict[Any, Any]: """Returns the bot_data from the pickle file if it exists or an empty :obj:`dict`. Returns: @@ -185,10 +189,10 @@ class PicklePersistence(BasePersistence): self.bot_data = data else: self.load_singlefile() - return deepcopy(self.bot_data) + return deepcopy(self.bot_data) # type: ignore[arg-type] - def get_conversations(self, name): - """Returns the conversations from the pickle file if it exists or an empty :obj:`dict`. + def get_conversations(self, name: str) -> ConversationDict: + """Returns the conversations from the pickle file if it exsists or an empty dict. Args: name (:obj:`str`): The handlers name. @@ -206,9 +210,11 @@ class PicklePersistence(BasePersistence): self.conversations = data else: self.load_singlefile() - return self.conversations.get(name, {}).copy() + return self.conversations.get(name, {}).copy() # type: ignore[union-attr] - def update_conversation(self, name, key, new_state): + def update_conversation(self, + name: str, key: Tuple[int, ...], + new_state: Optional[object]) -> None: """Will update the conversations for the given handler and depending on :attr:`on_flush` save the pickle file. @@ -217,6 +223,8 @@ class PicklePersistence(BasePersistence): key (:obj:`tuple`): The key the state is changed for. new_state (:obj:`tuple` | :obj:`any`): The new state for the given key. """ + if not self.conversations: + self.conversations = dict() if self.conversations.setdefault(name, {}).get(key) == new_state: return self.conversations[name][key] = new_state @@ -227,7 +235,7 @@ class PicklePersistence(BasePersistence): else: self.dump_singlefile() - def update_user_data(self, user_id, data): + def update_user_data(self, user_id: int, data: Dict) -> None: """Will update the user_data and depending on :attr:`on_flush` save the pickle file. Args: @@ -246,7 +254,7 @@ class PicklePersistence(BasePersistence): else: self.dump_singlefile() - def update_chat_data(self, chat_id, data): + def update_chat_data(self, chat_id: int, data: Dict) -> None: """Will update the chat_data and depending on :attr:`on_flush` save the pickle file. Args: @@ -265,7 +273,7 @@ class PicklePersistence(BasePersistence): else: self.dump_singlefile() - def update_bot_data(self, data): + def update_bot_data(self, data: Dict) -> None: """Will update the bot_data and depending on :attr:`on_flush` save the pickle file. Args: @@ -281,7 +289,7 @@ class PicklePersistence(BasePersistence): else: self.dump_singlefile() - def flush(self): + 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 9fe515cad..5a779ff42 100644 --- a/telegram/ext/pollanswerhandler.py +++ b/telegram/ext/pollanswerhandler.py @@ -20,6 +20,8 @@ from telegram import Update from .handler import Handler +from telegram.utils.types import HandlerArg + class PollAnswerHandler(Handler): """Handler class to handle Telegram updates that contain a poll answer. @@ -79,7 +81,7 @@ class PollAnswerHandler(Handler): """ - def check_update(self, update): + def check_update(self, update: HandlerArg) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -89,4 +91,4 @@ class PollAnswerHandler(Handler): :obj:`bool` """ - return isinstance(update, Update) and update.poll_answer + return isinstance(update, Update) and bool(update.poll_answer) diff --git a/telegram/ext/pollhandler.py b/telegram/ext/pollhandler.py index 6a5d6df4b..10ad32e87 100644 --- a/telegram/ext/pollhandler.py +++ b/telegram/ext/pollhandler.py @@ -20,6 +20,8 @@ from telegram import Update from .handler import Handler +from telegram.utils.types import HandlerArg + class PollHandler(Handler): """Handler class to handle Telegram updates that contain a poll. @@ -79,7 +81,7 @@ class PollHandler(Handler): """ - def check_update(self, update): + def check_update(self, update: HandlerArg) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -89,4 +91,4 @@ class PollHandler(Handler): :obj:`bool` """ - return isinstance(update, Update) and update.poll + return isinstance(update, Update) and bool(update.poll) diff --git a/telegram/ext/precheckoutqueryhandler.py b/telegram/ext/precheckoutqueryhandler.py index 6d33d73da..0fb552c7e 100644 --- a/telegram/ext/precheckoutqueryhandler.py +++ b/telegram/ext/precheckoutqueryhandler.py @@ -21,6 +21,8 @@ from telegram import Update from .handler import Handler +from telegram.utils.types import HandlerArg + class PreCheckoutQueryHandler(Handler): """Handler class to handle Telegram PreCheckout callback queries. @@ -80,7 +82,7 @@ class PreCheckoutQueryHandler(Handler): """ - def check_update(self, update): + def check_update(self, update: HandlerArg) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -90,4 +92,4 @@ class PreCheckoutQueryHandler(Handler): :obj:`bool` """ - return isinstance(update, Update) and update.pre_checkout_query + return isinstance(update, Update) and bool(update.pre_checkout_query) diff --git a/telegram/ext/regexhandler.py b/telegram/ext/regexhandler.py index 1a32c5200..f39d76564 100644 --- a/telegram/ext/regexhandler.py +++ b/telegram/ext/regexhandler.py @@ -25,6 +25,13 @@ from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.ext import MessageHandler, Filters +from telegram.utils.types import HandlerArg +from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict, Pattern +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class RegexHandler(MessageHandler): """Handler class to handle Telegram updates based on a regex. @@ -102,19 +109,19 @@ class RegexHandler(MessageHandler): """ def __init__(self, - pattern, - callback, - pass_groups=False, - pass_groupdict=False, - pass_update_queue=False, - pass_job_queue=False, - pass_user_data=False, - pass_chat_data=False, - allow_edited=False, - message_updates=True, - channel_post_updates=False, - edited_updates=False, - run_async=False): + pattern: Union[str, Pattern], + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + allow_edited: bool = False, + message_updates: bool = True, + channel_post_updates: bool = False, + edited_updates: bool = False, + run_async: bool = False): warnings.warn('RegexHandler is deprecated. See https://git.io/fxJuV for more info', TelegramDeprecationWarning, stacklevel=2) @@ -131,10 +138,15 @@ class RegexHandler(MessageHandler): self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - def collect_optional_args(self, dispatcher, update=None, check_result=None): + def collect_optional_args( + self, + dispatcher: 'Dispatcher', + update: HandlerArg = None, + check_result: Optional[Union[bool, Dict[str, Any]]] = None) -> Dict[str, Any]: optional_args = super().collect_optional_args(dispatcher, update, check_result) - if self.pass_groups: - optional_args['groups'] = check_result['matches'][0].groups() - if self.pass_groupdict: - optional_args['groupdict'] = check_result['matches'][0].groupdict() + if isinstance(check_result, dict): + if self.pass_groups: + optional_args['groups'] = check_result['matches'][0].groups() + if self.pass_groupdict: + optional_args['groupdict'] = check_result['matches'][0].groupdict() return optional_args diff --git a/telegram/ext/shippingqueryhandler.py b/telegram/ext/shippingqueryhandler.py index 5b335d196..527b4cb3c 100644 --- a/telegram/ext/shippingqueryhandler.py +++ b/telegram/ext/shippingqueryhandler.py @@ -21,6 +21,8 @@ from telegram import Update from .handler import Handler +from telegram.utils.types import HandlerArg + class ShippingQueryHandler(Handler): """Handler class to handle Telegram shipping callback queries. @@ -80,7 +82,7 @@ class ShippingQueryHandler(Handler): """ - def check_update(self, update): + def check_update(self, update: HandlerArg) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -90,4 +92,4 @@ class ShippingQueryHandler(Handler): :obj:`bool` """ - return isinstance(update, Update) and update.shipping_query + return isinstance(update, Update) and bool(update.shipping_query) diff --git a/telegram/ext/stringcommandhandler.py b/telegram/ext/stringcommandhandler.py index 991de26fa..4050f16cf 100644 --- a/telegram/ext/stringcommandhandler.py +++ b/telegram/ext/stringcommandhandler.py @@ -20,6 +20,13 @@ from .handler import Handler +from telegram.utils.types import HandlerArg +from typing import Callable, TYPE_CHECKING, Any, Optional, TypeVar, Dict, List +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class StringCommandHandler(Handler): """Handler class to handle string commands. Commands are string updates that start with ``/``. @@ -44,6 +51,7 @@ class StringCommandHandler(Handler): run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Args: + command (:obj:`str`): The command this handler should listen for. callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: @@ -73,12 +81,12 @@ class StringCommandHandler(Handler): """ def __init__(self, - command, - callback, - pass_args=False, - pass_update_queue=False, - pass_job_queue=False, - run_async=False): + command: str, + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + pass_args: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + run_async: bool = False): super().__init__( callback, pass_update_queue=pass_update_queue, @@ -87,7 +95,7 @@ class StringCommandHandler(Handler): self.command = command self.pass_args = pass_args - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[List[str]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -101,12 +109,20 @@ class StringCommandHandler(Handler): args = update[1:].split(' ') if args[0] == self.command: return args[1:] + return None - def collect_optional_args(self, dispatcher, update=None, check_result=None): + def collect_optional_args(self, + dispatcher: 'Dispatcher', + update: HandlerArg = None, + check_result: Optional[List[str]] = None) -> Dict[str, Any]: optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pass_args: optional_args['args'] = check_result return optional_args - def collect_additional_context(self, context, update, dispatcher, check_result): + def collect_additional_context(self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Optional[List[str]]) -> None: context.args = check_result diff --git a/telegram/ext/stringregexhandler.py b/telegram/ext/stringregexhandler.py index d5a505ef4..0a99015ab 100644 --- a/telegram/ext/stringregexhandler.py +++ b/telegram/ext/stringregexhandler.py @@ -22,6 +22,13 @@ import re from .handler import Handler +from typing import Callable, TYPE_CHECKING, Optional, TypeVar, Match, Dict, Any, Union, Pattern +from telegram.utils.types import HandlerArg +if TYPE_CHECKING: + from telegram.ext import CallbackContext, Dispatcher + +RT = TypeVar('RT') + class StringRegexHandler(Handler): """Handler class to handle string updates based on a regex which checks the update content. @@ -84,13 +91,13 @@ class StringRegexHandler(Handler): """ def __init__(self, - pattern, - callback, - pass_groups=False, - pass_groupdict=False, - pass_update_queue=False, - pass_job_queue=False, - run_async=False): + pattern: Union[str, Pattern], + callback: Callable[[HandlerArg, 'CallbackContext'], RT], + pass_groups: bool = False, + pass_groupdict: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + run_async: bool = False): super().__init__( callback, pass_update_queue=pass_update_queue, @@ -104,7 +111,7 @@ class StringRegexHandler(Handler): self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict - def check_update(self, update): + def check_update(self, update: HandlerArg) -> Optional[Match]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: @@ -118,16 +125,24 @@ class StringRegexHandler(Handler): match = re.match(self.pattern, update) if match: return match + return None - def collect_optional_args(self, dispatcher, update=None, check_result=None): + def collect_optional_args(self, + dispatcher: 'Dispatcher', + update: HandlerArg = None, + check_result: Optional[Match] = None) -> Dict[str, Any]: optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pattern: - if self.pass_groups: + if self.pass_groups and check_result: optional_args['groups'] = check_result.groups() - if self.pass_groupdict: + if self.pass_groupdict and check_result: optional_args['groupdict'] = check_result.groupdict() return optional_args - def collect_additional_context(self, context, update, dispatcher, check_result): - if self.pattern: + def collect_additional_context(self, + context: 'CallbackContext', + update: HandlerArg, + dispatcher: 'Dispatcher', + check_result: Optional[Match]) -> None: + if self.pattern and check_result: context.matches = [check_result] diff --git a/telegram/ext/typehandler.py b/telegram/ext/typehandler.py index ff968d0ef..64c73b466 100644 --- a/telegram/ext/typehandler.py +++ b/telegram/ext/typehandler.py @@ -21,6 +21,14 @@ from .handler import Handler +from typing import Callable, TYPE_CHECKING, TypeVar, Type, Any + +if TYPE_CHECKING: + from telegram.ext import CallbackContext + +RT = TypeVar('RT') + + class TypeHandler(Handler): """Handler class to handle updates of custom types. @@ -67,12 +75,12 @@ class TypeHandler(Handler): """ def __init__(self, - type, - callback, - strict=False, - pass_update_queue=False, - pass_job_queue=False, - run_async=False): + type: Type, + callback: Callable[[Any, 'CallbackContext'], RT], + strict: bool = False, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + run_async: bool = False): super().__init__( callback, pass_update_queue=pass_update_queue, @@ -81,7 +89,7 @@ class TypeHandler(Handler): self.type = type self.strict = strict - def check_update(self, update): + def check_update(self, update: Any) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index 05781f7ca..203695e31 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -34,6 +34,11 @@ from telegram.utils.helpers import get_signal_name from telegram.utils.request import Request from telegram.utils.webhookhandler import (WebhookServer, WebhookAppClass) +from typing import Callable, Dict, TYPE_CHECKING, Any, List, Union, Tuple, no_type_check, Optional + +if TYPE_CHECKING: + from telegram.ext import BasePersistence, Defaults + class Updater: """ @@ -104,19 +109,19 @@ class Updater: _request = None def __init__(self, - token=None, - base_url=None, - workers=4, - bot=None, - private_key=None, - private_key_password=None, - user_sig_handler=None, - request_kwargs=None, - persistence=None, - defaults=None, - use_context=True, - dispatcher=None, - base_file_url=None): + 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: Dispatcher = None, + base_file_url: str = None): if defaults and bot: warnings.warn('Passing defaults to an Updater has no effect when a Bot is passed ' @@ -164,14 +169,14 @@ class Updater: if 'con_pool_size' not in request_kwargs: request_kwargs['con_pool_size'] = con_pool_size self._request = Request(**request_kwargs) - self.bot = Bot(token, + self.bot = Bot(token, # type: ignore[arg-type] base_url, base_file_url=base_file_url, request=self._request, private_key=private_key, private_key_password=private_key_password, defaults=defaults) - self.update_queue = Queue() + self.update_queue: Queue = Queue() self.job_queue = JobQueue() self.__exception_event = Event() self.persistence = persistence @@ -203,9 +208,9 @@ class Updater: self.is_idle = False self.httpd = None self.__lock = Lock() - self.__threads = [] + self.__threads: List[Thread] = [] - def _init_thread(self, target, name, *args, **kwargs): + def _init_thread(self, target: Callable, name: str, *args: Any, **kwargs: Any) -> None: thr = Thread(target=self._thread_wrapper, name="Bot:{}:{}".format(self.bot.id, name), args=(target,) + args, @@ -213,7 +218,7 @@ class Updater: thr.start() self.__threads.append(thr) - def _thread_wrapper(self, target, *args, **kwargs): + def _thread_wrapper(self, target: Callable, *args: Any, **kwargs: Any) -> None: thr_name = current_thread().name self.logger.debug('{} - started'.format(thr_name)) try: @@ -225,12 +230,12 @@ class Updater: self.logger.debug('{} - ended'.format(thr_name)) def start_polling(self, - poll_interval=0.0, - timeout=10, - clean=False, - bootstrap_retries=-1, - read_latency=2., - allowed_updates=None): + poll_interval: float = 0.0, + timeout: float = 10, + clean: bool = False, + bootstrap_retries: int = -1, + read_latency: float = 2., + allowed_updates: List[str] = None) -> Optional[Queue]: """Starts polling updates from Telegram. Args: @@ -275,18 +280,19 @@ class Updater: # Return the update queue so the main thread can insert updates return self.update_queue + return None def start_webhook(self, - listen='127.0.0.1', - port=80, - url_path='', - cert=None, - key=None, - clean=False, - bootstrap_retries=0, - webhook_url=None, - allowed_updates=None, - force_event_loop=False): + listen: str = '127.0.0.1', + port: int = 80, + url_path: str = '', + cert: str = None, + key: str = None, + clean: bool = False, + bootstrap_retries: int = 0, + webhook_url: str = None, + allowed_updates: List[str] = None, + force_event_loop: bool = False) -> Optional[Queue]: """ Starts a small http server to listen for updates via webhook. If cert and key are not provided, the webhook will be started directly on @@ -348,7 +354,9 @@ class Updater: # Return the update queue so the main thread can insert updates return self.update_queue + return None + @no_type_check def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean, allowed_updates, ready=None): # pragma: no cover # Thread target of thread 'updater'. Runs in background, pulls @@ -388,6 +396,7 @@ class Updater: self._network_loop_retry(polling_action_cb, polling_onerr_cb, 'getting Updates', poll_interval) + @no_type_check def _network_loop_retry(self, action_cb, onerr_cb, description, interval): """Perform a loop calling `action_cb`, retrying after network errors. @@ -430,7 +439,7 @@ class Updater: sleep(cur_interval) @staticmethod - def _increase_poll_interval(current_interval): + def _increase_poll_interval(current_interval: float) -> float: # increase waiting times on subsequent errors up to 30secs if current_interval == 0: current_interval = 1 @@ -440,6 +449,7 @@ class Updater: current_interval = 30 return current_interval + @no_type_check def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean, webhook_url, allowed_updates, ready=None, force_event_loop=False): self.logger.debug('Updater thread started (webhook)') @@ -481,9 +491,10 @@ class Updater: self.httpd.serve_forever(force_event_loop=force_event_loop, ready=ready) @staticmethod - def _gen_webhook_url(listen, port, url_path): + def _gen_webhook_url(listen: str, port: int, url_path: str) -> str: return 'https://{listen}:{port}{path}'.format(listen=listen, port=port, path=url_path) + @no_type_check def _bootstrap(self, max_retries, clean, @@ -541,7 +552,7 @@ class Updater: self._network_loop_retry(bootstrap_set_webhook, bootstrap_onerr_cb, 'bootstrap set webhook', bootstrap_interval) - def stop(self): + def stop(self) -> None: """Stops the polling/webhook thread, the dispatcher and the job queue.""" self.job_queue.stop() @@ -559,7 +570,8 @@ class Updater: if self._request: self._request.stop() - def _stop_httpd(self): + @no_type_check + def _stop_httpd(self) -> None: if self.httpd: self.logger.debug('Waiting for current webhook connection to be ' 'closed... Send a Telegram message to the bot to exit ' @@ -567,18 +579,21 @@ class Updater: self.httpd.shutdown() self.httpd = None - def _stop_dispatcher(self): + @no_type_check + def _stop_dispatcher(self) -> None: self.logger.debug('Requesting Dispatcher to stop...') self.dispatcher.stop() - def _join_threads(self): + @no_type_check + def _join_threads(self) -> None: for thr in self.__threads: self.logger.debug('Waiting for {} thread to end'.format(thr.name)) thr.join() self.logger.debug('{} thread has ended'.format(thr.name)) self.__threads = [] - def signal_handler(self, signum, frame): + @no_type_check + def signal_handler(self, signum, frame) -> None: self.is_idle = False if self.running: self.logger.info('Received signal {} ({}), stopping...'.format( @@ -595,7 +610,7 @@ class Updater: import os os._exit(1) - def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)): + def idle(self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)) -> None: """Blocks until one of the signals are received and stops the updater. Args: diff --git a/telegram/files/animation.py b/telegram/files/animation.py index 43f95ce64..88bcac5a4 100644 --- a/telegram/files/animation.py +++ b/telegram/files/animation.py @@ -20,6 +20,11 @@ from telegram import PhotoSize from telegram import TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File + class Animation(TelegramObject): """This object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound). @@ -60,17 +65,17 @@ class Animation(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - width, - height, - duration, - thumb=None, - file_name=None, - mime_type=None, - file_size=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + width: int, + height: int, + duration: int, + thumb: PhotoSize = None, + file_name: str = None, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -87,17 +92,17 @@ class Animation(TelegramObject): self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Animation']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: diff --git a/telegram/files/audio.py b/telegram/files/audio.py index 2610d791a..273a63b5d 100644 --- a/telegram/files/audio.py +++ b/telegram/files/audio.py @@ -20,6 +20,11 @@ from telegram import TelegramObject, PhotoSize +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File + class Audio(TelegramObject): """This object represents an audio file to be treated as music by the Telegram clients. @@ -61,16 +66,16 @@ class Audio(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - duration, - performer=None, - title=None, - mime_type=None, - file_size=None, - thumb=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + duration: int, + performer: str = None, + title: str = None, + mime_type: str = None, + file_size: int = None, + thumb: PhotoSize = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -86,7 +91,9 @@ class Audio(TelegramObject): self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Audio']: + data = cls.parse_data(data) + if not data: return None @@ -94,7 +101,7 @@ class Audio(TelegramObject): return cls(bot=bot, **data) - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: diff --git a/telegram/files/chatphoto.py b/telegram/files/chatphoto.py index 04d234ca6..3f97b4b02 100644 --- a/telegram/files/chatphoto.py +++ b/telegram/files/chatphoto.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram ChatPhoto.""" from telegram import TelegramObject +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File + class ChatPhoto(TelegramObject): """This object represents a chat photo. @@ -58,11 +62,12 @@ class ChatPhoto(TelegramObject): """ def __init__(self, - small_file_id, - small_file_unique_id, - big_file_id, - big_file_unique_id, - bot=None, **kwargs): + small_file_id: str, + small_file_unique_id: str, + big_file_id: str, + big_file_unique_id: str, + bot: 'Bot' = None, + **kwargs: Any): self.small_file_id = small_file_id self.small_file_unique_id = small_file_unique_id self.big_file_id = big_file_id @@ -72,14 +77,7 @@ class ChatPhoto(TelegramObject): self._id_attrs = (self.small_file_unique_id, self.big_file_unique_id,) - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - - def get_small_file(self, timeout=None, **kwargs): + def get_small_file(self, timeout: int = None, **kwargs: Any) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the small (160x160) chat photo @@ -99,7 +97,7 @@ class ChatPhoto(TelegramObject): """ return self.bot.get_file(self.small_file_id, timeout=timeout, **kwargs) - def get_big_file(self, timeout=None, **kwargs): + def get_big_file(self, timeout: int = None, **kwargs: Any) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the big (640x640) chat photo diff --git a/telegram/files/contact.py b/telegram/files/contact.py index 5cb6db3f4..c17d5cd5d 100644 --- a/telegram/files/contact.py +++ b/telegram/files/contact.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram Contact.""" from telegram import TelegramObject +from typing import Any class Contact(TelegramObject): @@ -44,8 +45,13 @@ class Contact(TelegramObject): """ - def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard=None, - **kwargs): + def __init__(self, + phone_number: str, + first_name: str, + last_name: str = None, + user_id: int = None, + vcard: str = None, + **kwargs: Any): # Required self.phone_number = str(phone_number) self.first_name = first_name @@ -55,10 +61,3 @@ class Contact(TelegramObject): self.vcard = vcard self._id_attrs = (self.phone_number,) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegram/files/document.py b/telegram/files/document.py index 8600fea90..72b4abe57 100644 --- a/telegram/files/document.py +++ b/telegram/files/document.py @@ -20,6 +20,11 @@ from telegram import PhotoSize, TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File + class Document(TelegramObject): """This object represents a general file @@ -55,14 +60,14 @@ class Document(TelegramObject): _id_keys = ('file_id',) def __init__(self, - file_id, - file_unique_id, - thumb=None, - file_name=None, - mime_type=None, - file_size=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + thumb: PhotoSize = None, + file_name: str = None, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -76,17 +81,17 @@ class Document(TelegramObject): self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Document']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: diff --git a/telegram/files/file.py b/telegram/files/file.py index 3a18d9fe7..6b9298206 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -26,6 +26,10 @@ import urllib.parse as urllib_parse from telegram import TelegramObject from telegram.passport.credentials import decrypt +from typing import Any, Optional, IO, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, FileCredentials + class File(TelegramObject): """ @@ -65,12 +69,12 @@ class File(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - bot=None, - file_size=None, - file_path=None, - **kwargs): + file_id: str, + file_unique_id: str, + bot: 'Bot' = None, + file_size: int = None, + file_path: str = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -78,18 +82,14 @@ class File(TelegramObject): self.file_size = file_size self.file_path = file_path self.bot = bot - self._credentials = None + self._credentials: Optional['FileCredentials'] = None self._id_attrs = (self.file_unique_id,) - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - - def download(self, custom_path=None, out=None, timeout=None): + def download(self, + custom_path: str = None, + out: IO = None, + timeout: int = None) -> Union[str, IO]: """ Download this file. By default, the file is saved in the current working directory with its original filename as reported by Telegram. If the file has no filename, it the file ID will @@ -147,13 +147,13 @@ class File(TelegramObject): fobj.write(buf) return filename - def _get_encoded_url(self): + def _get_encoded_url(self) -> str: """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" sres = urllib_parse.urlsplit(self.file_path) return urllib_parse.urlunsplit(urllib_parse.SplitResult( sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment)) - def download_as_bytearray(self, buf=None): + def download_as_bytearray(self, buf: bytearray = None) -> bytes: """Download this file and return it as a bytearray. Args: @@ -170,5 +170,5 @@ class File(TelegramObject): buf.extend(self.bot.request.retrieve(self._get_encoded_url())) return buf - def set_credentials(self, credentials): + def set_credentials(self, credentials: 'FileCredentials') -> None: self._credentials = credentials diff --git a/telegram/files/inputfile.py b/telegram/files/inputfile.py index 7d052ac87..8c57c64ef 100644 --- a/telegram/files/inputfile.py +++ b/telegram/files/inputfile.py @@ -26,6 +26,8 @@ from uuid import uuid4 from telegram import TelegramError +from typing import IO, Tuple, Optional + DEFAULT_MIME_TYPE = 'application/octet-stream' @@ -48,7 +50,7 @@ class InputFile: """ - def __init__(self, obj, filename=None, attach=None): + def __init__(self, obj: IO, filename: str = None, attach: bool = None): self.filename = None self.input_file_content = obj.read() self.attach = 'attached' + uuid4().hex if attach else None @@ -70,15 +72,15 @@ class InputFile: self.filename = self.mimetype.replace('/', '.') @property - def field_tuple(self): + def field_tuple(self) -> Tuple[str, bytes, str]: return self.filename, self.input_file_content, self.mimetype @staticmethod - def is_image(stream): + def is_image(stream: bytes) -> str: """Check if the content file is an image by analyzing its headers. Args: - stream (:obj:`str`): A str representing the content of a file. + stream (:obj:`bytes`): A byte stream representing the content of a file. Returns: :obj:`str`: The str mime-type of an image. @@ -91,9 +93,10 @@ class InputFile: raise TelegramError('Could not parse file content') @staticmethod - def is_file(obj): + def is_file(obj: object) -> bool: return hasattr(obj, 'read') - def to_dict(self): + def to_dict(self) -> Optional[str]: if self.attach: return 'attach://' + self.attach + return None diff --git a/telegram/files/inputmedia.py b/telegram/files/inputmedia.py index d4ec84703..1aba083e5 100644 --- a/telegram/files/inputmedia.py +++ b/telegram/files/inputmedia.py @@ -19,7 +19,11 @@ """Base class for Telegram InputMedia Objects.""" from telegram import TelegramObject, InputFile, PhotoSize, Animation, Video, Audio, Document -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue + +from typing import Union, IO, cast + +from telegram.utils.types import FileLike class InputMedia(TelegramObject): @@ -73,29 +77,32 @@ class InputMediaAnimation(InputMedia): """ def __init__(self, - media, - thumb=None, - caption=None, - parse_mode=DEFAULT_NONE, - width=None, - height=None, - duration=None): + media: Union[str, FileLike, Animation], + thumb: FileLike = None, + caption: str = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + width: int = None, + height: int = None, + duration: int = None): self.type = 'animation' if isinstance(media, Animation): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id self.width = media.width self.height = media.height self.duration = media.duration elif InputFile.is_file(media): + media = cast(IO, media) self.media = InputFile(media, attach=True) else: - self.media = media + self.media = media # type: ignore[assignment] if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + if InputFile.is_file(thumb): + thumb = cast(IO, thumb) + self.thumb = InputFile(thumb, attach=True) + else: + self.thumb = thumb # type: ignore[assignment] if caption: self.caption = caption @@ -129,15 +136,19 @@ class InputMediaPhoto(InputMedia): in :class:`telegram.ParseMode` for the available modes. """ - def __init__(self, media, caption=None, parse_mode=DEFAULT_NONE): + def __init__(self, + media: Union[str, FileLike, PhotoSize], + caption: str = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE): self.type = 'photo' if isinstance(media, PhotoSize): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id elif InputFile.is_file(media): + media = cast(IO, media) self.media = InputFile(media, attach=True) else: - self.media = media + self.media = media # type: ignore[assignment] if caption: self.caption = caption @@ -189,24 +200,34 @@ class InputMediaVideo(InputMedia): by Telegram. """ - def __init__(self, media, caption=None, width=None, height=None, duration=None, - supports_streaming=None, parse_mode=DEFAULT_NONE, thumb=None): + def __init__(self, + media: Union[str, FileLike, Video], + caption: str = None, + width: int = None, + height: int = None, + duration: int = None, + supports_streaming: bool = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + thumb: FileLike = None): self.type = 'video' if isinstance(media, Video): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id self.width = media.width self.height = media.height self.duration = media.duration elif InputFile.is_file(media): + media = cast(IO, media) self.media = InputFile(media, attach=True) else: - self.media = media + self.media = media # type: ignore[assignment] if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + if InputFile.is_file(thumb): + thumb = cast(IO, thumb) + self.thumb = InputFile(thumb, attach=True) + else: + self.thumb = thumb # type: ignore[assignment] if caption: self.caption = caption @@ -261,24 +282,33 @@ class InputMediaAudio(InputMedia): optional arguments. """ - def __init__(self, media, thumb=None, caption=None, parse_mode=DEFAULT_NONE, - duration=None, performer=None, title=None): + def __init__(self, + media: Union[str, FileLike, Audio], + thumb: FileLike = None, + caption: str = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + duration: int = None, + performer: str = None, + title: str = None): self.type = 'audio' if isinstance(media, Audio): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id self.duration = media.duration self.performer = media.performer self.title = media.title elif InputFile.is_file(media): + media = cast(IO, media) self.media = InputFile(media, attach=True) else: - self.media = media + self.media = media # type: ignore[assignment] if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + if InputFile.is_file(thumb): + thumb = cast(IO, thumb) + self.thumb = InputFile(thumb, attach=True) + else: + self.thumb = thumb # type: ignore[assignment] if caption: self.caption = caption @@ -318,20 +348,27 @@ class InputMediaDocument(InputMedia): Thumbnails can't be reused and can be only uploaded as a new file. """ - def __init__(self, media, thumb=None, caption=None, parse_mode=DEFAULT_NONE): + def __init__(self, + media: Union[str, FileLike, Document], + thumb: FileLike = None, + caption: str = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE): self.type = 'document' if isinstance(media, Document): - self.media = media.file_id + self.media: Union[str, InputFile] = media.file_id elif InputFile.is_file(media): + media = cast(IO, media) self.media = InputFile(media, attach=True) else: - self.media = media + self.media = media # type: ignore[assignment] if thumb: - self.thumb = thumb - if InputFile.is_file(self.thumb): - self.thumb = InputFile(self.thumb, attach=True) + if InputFile.is_file(thumb): + thumb = cast(IO, thumb) + self.thumb = InputFile(thumb, attach=True) + else: + self.thumb = thumb # type: ignore[assignment] if caption: self.caption = caption diff --git a/telegram/files/location.py b/telegram/files/location.py index ad719db24..ad23fe331 100644 --- a/telegram/files/location.py +++ b/telegram/files/location.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram Location.""" from telegram import TelegramObject +from typing import Any class Location(TelegramObject): @@ -38,16 +39,9 @@ class Location(TelegramObject): """ - def __init__(self, longitude, latitude, **kwargs): + def __init__(self, longitude: float, latitude: float, **kwargs: Any): # Required self.longitude = float(longitude) self.latitude = float(latitude) self._id_attrs = (self.longitude, self.latitude) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegram/files/photosize.py b/telegram/files/photosize.py index ae7b4a50f..f6504b05d 100644 --- a/telegram/files/photosize.py +++ b/telegram/files/photosize.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram PhotoSize.""" from telegram import TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File class PhotoSize(TelegramObject): @@ -52,13 +56,13 @@ class PhotoSize(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - width, - height, - file_size=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + width: int, + height: int, + file_size: int = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -70,25 +74,7 @@ class PhotoSize(TelegramObject): self._id_attrs = (self.file_unique_id,) - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - - @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - photos = list() - for photo in data: - photos.append(cls.de_json(photo, bot)) - - return photos - - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: diff --git a/telegram/files/sticker.py b/telegram/files/sticker.py index a4c903be7..4c504f574 100644 --- a/telegram/files/sticker.py +++ b/telegram/files/sticker.py @@ -19,6 +19,10 @@ """This module contains objects that represents stickers.""" from telegram import PhotoSize, TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, List, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File class Sticker(TelegramObject): @@ -68,18 +72,18 @@ class Sticker(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - width, - height, - is_animated, - thumb=None, - emoji=None, - file_size=None, - set_name=None, - mask_position=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + width: int, + height: int, + is_animated: bool, + thumb: PhotoSize = None, + emoji: str = None, + file_size: int = None, + set_name: str = None, + mask_position: 'MaskPosition' = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -97,25 +101,18 @@ class Sticker(TelegramObject): self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Sticker']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['mask_position'] = MaskPosition.de_json(data.get('mask_position'), bot) return cls(bot=bot, **data) - @classmethod - def de_list(cls, data, bot): - if not data: - return list() - - return [cls.de_json(d, bot) for d in data] - - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: str = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: @@ -161,8 +158,15 @@ class StickerSet(TelegramObject): """ - def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, thumb=None, - **kwargs): + def __init__(self, + name: str, + title: str, + is_animated: bool, + contains_masks: bool, + stickers: List[Sticker], + bot: 'Bot' = None, + thumb: PhotoSize = None, + **kwargs: Any): self.name = name self.title = title self.is_animated = is_animated @@ -174,18 +178,16 @@ class StickerSet(TelegramObject): self._id_attrs = (self.name,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['StickerSet']: if not data: return None - data = super().de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['stickers'] = Sticker.de_list(data.get('stickers'), bot) return cls(bot=bot, **data) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['stickers'] = [s.to_dict() for s in data.get('stickers')] @@ -225,16 +227,16 @@ class MaskPosition(TelegramObject): scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size. """ - FOREHEAD = 'forehead' + FOREHEAD: str = 'forehead' """:obj:`str`: 'forehead'""" - EYES = 'eyes' + EYES: str = 'eyes' """:obj:`str`: 'eyes'""" - MOUTH = 'mouth' + MOUTH: str = 'mouth' """:obj:`str`: 'mouth'""" - CHIN = 'chin' + CHIN: str = 'chin' """:obj:`str`: 'chin'""" - def __init__(self, point, x_shift, y_shift, scale, **kwargs): + def __init__(self, point: str, x_shift: float, y_shift: float, scale: float, **kwargs: Any): self.point = point self.x_shift = x_shift self.y_shift = y_shift @@ -243,7 +245,9 @@ class MaskPosition(TelegramObject): self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MaskPosition']: + data = cls.parse_data(data) + if data is None: return None diff --git a/telegram/files/venue.py b/telegram/files/venue.py index 142a0e9bf..95b890d8c 100644 --- a/telegram/files/venue.py +++ b/telegram/files/venue.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram Venue.""" from telegram import TelegramObject, Location +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class Venue(TelegramObject): @@ -46,8 +50,13 @@ class Venue(TelegramObject): """ - def __init__(self, location, title, address, foursquare_id=None, foursquare_type=None, - **kwargs): + def __init__(self, + location: Location, + title: str, + address: str, + foursquare_id: str = None, + foursquare_type: str = None, + **kwargs: Any): # Required self.location = location self.title = title @@ -59,8 +68,8 @@ class Venue(TelegramObject): self._id_attrs = (self.location, self.title) @classmethod - def de_json(cls, data, bot): - data = super().de_json(data, bot) + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Venue']: + data = cls.parse_data(data) if not data: return None diff --git a/telegram/files/video.py b/telegram/files/video.py index 6ab356744..c2ec70b4d 100644 --- a/telegram/files/video.py +++ b/telegram/files/video.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram Video.""" from telegram import PhotoSize, TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File class Video(TelegramObject): @@ -58,16 +62,16 @@ class Video(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - width, - height, - duration, - thumb=None, - mime_type=None, - file_size=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + width: int, + height: int, + duration: int, + thumb: PhotoSize = None, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -83,17 +87,17 @@ class Video(TelegramObject): self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Video']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: diff --git a/telegram/files/videonote.py b/telegram/files/videonote.py index 657ab0e22..524c40d2e 100644 --- a/telegram/files/videonote.py +++ b/telegram/files/videonote.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram VideoNote.""" from telegram import PhotoSize, TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File class VideoNote(TelegramObject): @@ -55,14 +59,14 @@ class VideoNote(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - length, - duration, - thumb=None, - file_size=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + length: int, + duration: int, + thumb: PhotoSize = None, + file_size: int = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -76,17 +80,17 @@ class VideoNote(TelegramObject): self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['VideoNote']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: diff --git a/telegram/files/voice.py b/telegram/files/voice.py index 5cfc258de..c50b30ec6 100644 --- a/telegram/files/voice.py +++ b/telegram/files/voice.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram Voice.""" from telegram import TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File class Voice(TelegramObject): @@ -52,13 +56,13 @@ class Voice(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - duration, - mime_type=None, - file_size=None, - bot=None, - **kwargs): + file_id: str, + file_unique_id: str, + duration: int, + mime_type: str = None, + file_size: int = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) @@ -70,16 +74,7 @@ class Voice(TelegramObject): self._id_attrs = (self.file_unique_id,) - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - data = super().de_json(data, bot) - - return cls(bot=bot, **data) - - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` Args: diff --git a/telegram/forcereply.py b/telegram/forcereply.py index 963bc3d87..cd8ac7330 100644 --- a/telegram/forcereply.py +++ b/telegram/forcereply.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram ForceReply.""" from telegram import ReplyMarkup +from typing import Any class ForceReply(ReplyMarkup): @@ -48,7 +49,7 @@ class ForceReply(ReplyMarkup): """ - def __init__(self, force_reply=True, selective=False, **kwargs): + def __init__(self, force_reply: bool = True, selective: bool = False, **kwargs: Any): # Required self.force_reply = bool(force_reply) # Optionals diff --git a/telegram/games/game.py b/telegram/games/game.py index 754869edb..04f3acf04 100644 --- a/telegram/games/game.py +++ b/telegram/games/game.py @@ -21,6 +21,10 @@ import sys from telegram import MessageEntity, TelegramObject, Animation, PhotoSize +from telegram.utils.types import JSONDict +from typing import List, Any, Dict, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class Game(TelegramObject): @@ -63,13 +67,13 @@ class Game(TelegramObject): """ def __init__(self, - title, - description, - photo, - text=None, - text_entities=None, - animation=None, - **kwargs): + title: str, + description: str, + photo: List[PhotoSize], + text: str = None, + text_entities: List[MessageEntity] = None, + animation: Animation = None, + **kwargs: Any): # Required self.title = title self.description = description @@ -82,19 +86,19 @@ class Game(TelegramObject): self._id_attrs = (self.title, self.description, self.photo) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Game']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['photo'] = PhotoSize.de_list(data.get('photo'), bot) data['text_entities'] = MessageEntity.de_list(data.get('text_entities'), bot) data['animation'] = Animation.de_json(data.get('animation'), bot) return cls(**data) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['photo'] = [p.to_dict() for p in self.photo] @@ -103,7 +107,7 @@ class Game(TelegramObject): return data - def parse_text_entity(self, entity): + def parse_text_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. Note: @@ -118,7 +122,13 @@ class Game(TelegramObject): Returns: :obj:`str`: The text of the given entity. + Raises: + RuntimeError: If this game has no text. + """ + if not self.text: + raise RuntimeError("This Game has no 'text'.") + # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xffff: return self.text[entity.offset:entity.offset + entity.length] @@ -128,7 +138,7 @@ class Game(TelegramObject): return entity_text.decode('utf-16-le') - def parse_text_entities(self, types=None): + def parse_text_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message filtered by their ``type`` attribute as the key, and @@ -154,8 +164,8 @@ class Game(TelegramObject): return { entity: self.parse_text_entity(entity) - for entity in self.text_entities if entity.type in types + for entity in (self.text_entities or []) if entity.type in types } - def __hash__(self): + def __hash__(self) -> int: return hash((self.title, self.description, tuple(p for p in self.photo))) diff --git a/telegram/games/gamehighscore.py b/telegram/games/gamehighscore.py index 07ea872a6..096a28c2c 100644 --- a/telegram/games/gamehighscore.py +++ b/telegram/games/gamehighscore.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram GameHighScore.""" from telegram import TelegramObject, User +from telegram.utils.types import JSONDict +from typing import Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class GameHighScore(TelegramObject): @@ -39,7 +43,7 @@ class GameHighScore(TelegramObject): """ - def __init__(self, position, user, score): + def __init__(self, position: int, user: User, score: int): self.position = position self.user = user self.score = score @@ -47,12 +51,12 @@ class GameHighScore(TelegramObject): self._id_attrs = (self.position, self.user, self.score) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['GameHighScore']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['user'] = User.de_json(data.get('user'), bot) return cls(**data) diff --git a/telegram/inline/inlinekeyboardbutton.py b/telegram/inline/inlinekeyboardbutton.py index 3f558a75c..09373255e 100644 --- a/telegram/inline/inlinekeyboardbutton.py +++ b/telegram/inline/inlinekeyboardbutton.py @@ -19,6 +19,9 @@ """This module contains an object that represents a Telegram InlineKeyboardButton.""" from telegram import TelegramObject +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import CallbackGame, LoginUrl class InlineKeyboardButton(TelegramObject): @@ -79,15 +82,15 @@ class InlineKeyboardButton(TelegramObject): """ def __init__(self, - text, - url=None, - callback_data=None, - switch_inline_query=None, - switch_inline_query_current_chat=None, - callback_game=None, - pay=None, - login_url=None, - **kwargs): + text: str, + url: str = None, + callback_data: str = None, + switch_inline_query: str = None, + switch_inline_query_current_chat: str = None, + callback_game: 'CallbackGame' = None, + pay: bool = None, + login_url: 'LoginUrl' = None, + **kwargs: Any): # Required self.text = text @@ -110,10 +113,3 @@ class InlineKeyboardButton(TelegramObject): self.callback_game, self.pay, ) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegram/inline/inlinekeyboardmarkup.py b/telegram/inline/inlinekeyboardmarkup.py index e2a7fc999..6e7bda8cb 100644 --- a/telegram/inline/inlinekeyboardmarkup.py +++ b/telegram/inline/inlinekeyboardmarkup.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram InlineKeyboardMarkup.""" from telegram import ReplyMarkup, InlineKeyboardButton +from telegram.utils.types import JSONDict +from typing import Any, List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class InlineKeyboardMarkup(ReplyMarkup): @@ -39,11 +43,11 @@ class InlineKeyboardMarkup(ReplyMarkup): """ - def __init__(self, inline_keyboard, **kwargs): + def __init__(self, inline_keyboard: List[List[InlineKeyboardButton]], **kwargs: Any): # Required self.inline_keyboard = inline_keyboard - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['inline_keyboard'] = [] @@ -53,20 +57,26 @@ class InlineKeyboardMarkup(ReplyMarkup): return data @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], + bot: 'Bot') -> Optional['InlineKeyboardMarkup']: + data = cls.parse_data(data) + if not data: return None + keyboard = [] for row in data['inline_keyboard']: tmp = [] for col in row: - tmp.append(InlineKeyboardButton.de_json(col, bot)) + btn = InlineKeyboardButton.de_json(col, bot) + if btn: + tmp.append(btn) keyboard.append(tmp) return cls(keyboard) @classmethod - def from_button(cls, button, **kwargs): + def from_button(cls, button: InlineKeyboardButton, **kwargs: Any) -> 'InlineKeyboardMarkup': """Shortcut for:: InlineKeyboardMarkup([[button]], **kwargs) @@ -81,7 +91,8 @@ class InlineKeyboardMarkup(ReplyMarkup): return cls([[button]], **kwargs) @classmethod - def from_row(cls, button_row, **kwargs): + def from_row(cls, button_row: List[InlineKeyboardButton], + **kwargs: Any) -> 'InlineKeyboardMarkup': """Shortcut for:: InlineKeyboardMarkup([button_row], **kwargs) @@ -97,7 +108,8 @@ class InlineKeyboardMarkup(ReplyMarkup): return cls([button_row], **kwargs) @classmethod - def from_column(cls, button_column, **kwargs): + def from_column(cls, button_column: List[InlineKeyboardButton], + **kwargs: Any) -> 'InlineKeyboardMarkup': """Shortcut for:: InlineKeyboardMarkup([[button] for button in button_column], **kwargs) @@ -113,7 +125,7 @@ class InlineKeyboardMarkup(ReplyMarkup): button_grid = [[button] for button in button_column] return cls(button_grid, **kwargs) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): if len(self.inline_keyboard) != len(other.inline_keyboard): return False @@ -126,5 +138,5 @@ class InlineKeyboardMarkup(ReplyMarkup): return True return super(InlineKeyboardMarkup, self).__eq__(other) # pylint: disable=no-member - def __hash__(self): + def __hash__(self) -> int: return hash(tuple(tuple(button for button in row) for row in self.inline_keyboard)) diff --git a/telegram/inline/inlinequery.py b/telegram/inline/inlinequery.py index f77c7e9d0..b60a0f2a5 100644 --- a/telegram/inline/inlinequery.py +++ b/telegram/inline/inlinequery.py @@ -20,6 +20,10 @@ """This module contains an object that represents a Telegram InlineQuery.""" from telegram import TelegramObject, User, Location +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class InlineQuery(TelegramObject): @@ -53,7 +57,14 @@ class InlineQuery(TelegramObject): """ - def __init__(self, id, from_user, query, offset, location=None, bot=None, **kwargs): + def __init__(self, + id: str, + from_user: User, + query: str, + offset: str, + location: Location = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.id = id self.from_user = from_user @@ -67,8 +78,8 @@ class InlineQuery(TelegramObject): self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): - data = super().de_json(data, bot) + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['InlineQuery']: + data = cls.parse_data(data) if not data: return None @@ -78,7 +89,7 @@ class InlineQuery(TelegramObject): return cls(bot=bot, **data) - def answer(self, *args, auto_pagination=False, **kwargs): + def answer(self, *args: Any, auto_pagination: bool = False, **kwargs: Any) -> bool: """Shortcut for:: bot.answer_inline_query(update.inline_query.id, diff --git a/telegram/inline/inlinequeryresult.py b/telegram/inline/inlinequeryresult.py index 36483850f..b11809541 100644 --- a/telegram/inline/inlinequeryresult.py +++ b/telegram/inline/inlinequeryresult.py @@ -19,6 +19,7 @@ """This module contains the classes that represent Telegram InlineQueryResult.""" from telegram import TelegramObject +from typing import Any class InlineQueryResult(TelegramObject): @@ -38,7 +39,7 @@ class InlineQueryResult(TelegramObject): """ - def __init__(self, type, id, **kwargs): + def __init__(self, type: str, id: str, **kwargs: Any): # Required self.type = str(type) self.id = str(id) @@ -46,9 +47,9 @@ class InlineQueryResult(TelegramObject): self._id_attrs = (self.id,) @property - def _has_parse_mode(self): + def _has_parse_mode(self) -> bool: return hasattr(self, 'parse_mode') @property - def _has_input_message_content(self): + def _has_input_message_content(self) -> bool: return hasattr(self, 'input_message_content') diff --git a/telegram/inline/inlinequeryresultarticle.py b/telegram/inline/inlinequeryresultarticle.py index 6abb4dcd8..5f670faa2 100644 --- a/telegram/inline/inlinequeryresultarticle.py +++ b/telegram/inline/inlinequeryresultarticle.py @@ -19,6 +19,9 @@ """This module contains the classes that represent Telegram InlineQueryResultArticle.""" from telegram import InlineQueryResult +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultArticle(InlineQueryResult): @@ -59,17 +62,17 @@ class InlineQueryResultArticle(InlineQueryResult): """ def __init__(self, - id, - title, - input_message_content, - reply_markup=None, - url=None, - hide_url=None, - description=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - **kwargs): + id: str, + title: str, + input_message_content: 'InputMessageContent', + reply_markup: 'ReplyMarkup' = None, + url: str = None, + hide_url: bool = None, + description: str = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + **kwargs: Any): # Required super().__init__('article', id) diff --git a/telegram/inline/inlinequeryresultaudio.py b/telegram/inline/inlinequeryresultaudio.py index 1d2026e65..8ad0d8c1b 100644 --- a/telegram/inline/inlinequeryresultaudio.py +++ b/telegram/inline/inlinequeryresultaudio.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultAudio.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultAudio(InlineQueryResult): @@ -63,16 +66,16 @@ class InlineQueryResultAudio(InlineQueryResult): """ def __init__(self, - id, - audio_url, - title, - performer=None, - audio_duration=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + audio_url: str, + title: str, + performer: str = None, + audio_duration: int = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('audio', id) diff --git a/telegram/inline/inlinequeryresultcachedaudio.py b/telegram/inline/inlinequeryresultcachedaudio.py index eda2481ce..09ca76960 100644 --- a/telegram/inline/inlinequeryresultcachedaudio.py +++ b/telegram/inline/inlinequeryresultcachedaudio.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultCachedAudio.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedAudio(InlineQueryResult): @@ -57,13 +60,13 @@ class InlineQueryResultCachedAudio(InlineQueryResult): """ def __init__(self, - id, - audio_file_id, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + audio_file_id: str, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('audio', id) self.audio_file_id = audio_file_id diff --git a/telegram/inline/inlinequeryresultcacheddocument.py b/telegram/inline/inlinequeryresultcacheddocument.py index c3c923a8f..3a04b4991 100644 --- a/telegram/inline/inlinequeryresultcacheddocument.py +++ b/telegram/inline/inlinequeryresultcacheddocument.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultCachedDocument.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedDocument(InlineQueryResult): @@ -63,15 +66,15 @@ class InlineQueryResultCachedDocument(InlineQueryResult): """ def __init__(self, - id, - title, - document_file_id, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + title: str, + document_file_id: str, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('document', id) self.title = title diff --git a/telegram/inline/inlinequeryresultcachedgif.py b/telegram/inline/inlinequeryresultcachedgif.py index a688b1150..cb325132a 100644 --- a/telegram/inline/inlinequeryresultcachedgif.py +++ b/telegram/inline/inlinequeryresultcachedgif.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultCachedGif.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedGif(InlineQueryResult): @@ -62,14 +65,14 @@ class InlineQueryResultCachedGif(InlineQueryResult): """ def __init__(self, - id, - gif_file_id, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + gif_file_id: str, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('gif', id) self.gif_file_id = gif_file_id diff --git a/telegram/inline/inlinequeryresultcachedmpeg4gif.py b/telegram/inline/inlinequeryresultcachedmpeg4gif.py index 644045131..f71eef5a6 100644 --- a/telegram/inline/inlinequeryresultcachedmpeg4gif.py +++ b/telegram/inline/inlinequeryresultcachedmpeg4gif.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): @@ -62,14 +65,14 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): """ def __init__(self, - id, - mpeg4_file_id, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + mpeg4_file_id: str, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('mpeg4_gif', id) self.mpeg4_file_id = mpeg4_file_id diff --git a/telegram/inline/inlinequeryresultcachedphoto.py b/telegram/inline/inlinequeryresultcachedphoto.py index 8c41b3539..2f226d624 100644 --- a/telegram/inline/inlinequeryresultcachedphoto.py +++ b/telegram/inline/inlinequeryresultcachedphoto.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultPhoto""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedPhoto(InlineQueryResult): @@ -64,15 +67,15 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): """ def __init__(self, - id, - photo_file_id, - title=None, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + photo_file_id: str, + title: str = None, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('photo', id) self.photo_file_id = photo_file_id diff --git a/telegram/inline/inlinequeryresultcachedsticker.py b/telegram/inline/inlinequeryresultcachedsticker.py index d963e5465..1f024b08a 100644 --- a/telegram/inline/inlinequeryresultcachedsticker.py +++ b/telegram/inline/inlinequeryresultcachedsticker.py @@ -19,6 +19,9 @@ """This module contains the classes that represent Telegram InlineQueryResultCachedSticker.""" from telegram import InlineQueryResult +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import ReplyMarkup, InputMessageContent class InlineQueryResultCachedSticker(InlineQueryResult): @@ -48,11 +51,11 @@ class InlineQueryResultCachedSticker(InlineQueryResult): """ def __init__(self, - id, - sticker_file_id, - reply_markup=None, - input_message_content=None, - **kwargs): + id: str, + sticker_file_id: str, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + **kwargs: Any): # Required super().__init__('sticker', id) self.sticker_file_id = sticker_file_id diff --git a/telegram/inline/inlinequeryresultcachedvideo.py b/telegram/inline/inlinequeryresultcachedvideo.py index 8a6c574b3..b4d6f43b9 100644 --- a/telegram/inline/inlinequeryresultcachedvideo.py +++ b/telegram/inline/inlinequeryresultcachedvideo.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultCachedVideo.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedVideo(InlineQueryResult): @@ -64,15 +67,15 @@ class InlineQueryResultCachedVideo(InlineQueryResult): """ def __init__(self, - id, - video_file_id, - title, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + video_file_id: str, + title: str, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('video', id) self.video_file_id = video_file_id diff --git a/telegram/inline/inlinequeryresultcachedvoice.py b/telegram/inline/inlinequeryresultcachedvoice.py index 91bd11aa2..cd3c2a431 100644 --- a/telegram/inline/inlinequeryresultcachedvoice.py +++ b/telegram/inline/inlinequeryresultcachedvoice.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultCachedVoice.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultCachedVoice(InlineQueryResult): @@ -59,14 +62,14 @@ class InlineQueryResultCachedVoice(InlineQueryResult): """ def __init__(self, - id, - voice_file_id, - title, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + voice_file_id: str, + title: str, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('voice', id) self.voice_file_id = voice_file_id diff --git a/telegram/inline/inlinequeryresultcontact.py b/telegram/inline/inlinequeryresultcontact.py index 6233066b3..ca7640feb 100644 --- a/telegram/inline/inlinequeryresultcontact.py +++ b/telegram/inline/inlinequeryresultcontact.py @@ -19,6 +19,9 @@ """This module contains the classes that represent Telegram InlineQueryResultContact.""" from telegram import InlineQueryResult +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import ReplyMarkup, InputMessageContent class InlineQueryResultContact(InlineQueryResult): @@ -62,17 +65,17 @@ class InlineQueryResultContact(InlineQueryResult): """ def __init__(self, - id, - phone_number, - first_name, - last_name=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - vcard=None, - **kwargs): + id: str, + phone_number: str, + first_name: str, + last_name: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + vcard: str = None, + **kwargs: Any): # Required super().__init__('contact', id) self.phone_number = phone_number diff --git a/telegram/inline/inlinequeryresultdocument.py b/telegram/inline/inlinequeryresultdocument.py index 12be44d33..815e450fe 100644 --- a/telegram/inline/inlinequeryresultdocument.py +++ b/telegram/inline/inlinequeryresultdocument.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultDocument""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultDocument(InlineQueryResult): @@ -74,19 +77,19 @@ class InlineQueryResultDocument(InlineQueryResult): """ def __init__(self, - id, - document_url, - title, - mime_type, - caption=None, - description=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + document_url: str, + title: str, + mime_type: str, + caption: str = None, + description: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('document', id) self.document_url = document_url diff --git a/telegram/inline/inlinequeryresultgame.py b/telegram/inline/inlinequeryresultgame.py index fee463216..8b3ae38ba 100644 --- a/telegram/inline/inlinequeryresultgame.py +++ b/telegram/inline/inlinequeryresultgame.py @@ -19,6 +19,9 @@ """This module contains the classes that represent Telegram InlineQueryResultGame.""" from telegram import InlineQueryResult +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import ReplyMarkup class InlineQueryResultGame(InlineQueryResult): @@ -40,7 +43,11 @@ class InlineQueryResultGame(InlineQueryResult): """ - def __init__(self, id, game_short_name, reply_markup=None, **kwargs): + def __init__(self, + id: str, + game_short_name: str, + reply_markup: 'ReplyMarkup' = None, + **kwargs: Any): # Required super().__init__('game', id) self.id = id diff --git a/telegram/inline/inlinequeryresultgif.py b/telegram/inline/inlinequeryresultgif.py index 21bdb9687..ccd069d1e 100644 --- a/telegram/inline/inlinequeryresultgif.py +++ b/telegram/inline/inlinequeryresultgif.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultGif.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultGif(InlineQueryResult): @@ -74,19 +77,19 @@ class InlineQueryResultGif(InlineQueryResult): """ def __init__(self, - id, - gif_url, - thumb_url, - gif_width=None, - gif_height=None, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - gif_duration=None, - parse_mode=DEFAULT_NONE, - thumb_mime_type=None, - **kwargs): + id: str, + gif_url: str, + thumb_url: str, + gif_width: int = None, + gif_height: int = None, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + gif_duration: int = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + thumb_mime_type: str = None, + **kwargs: Any): # Required super().__init__('gif', id) diff --git a/telegram/inline/inlinequeryresultlocation.py b/telegram/inline/inlinequeryresultlocation.py index 0d315e109..50e866743 100644 --- a/telegram/inline/inlinequeryresultlocation.py +++ b/telegram/inline/inlinequeryresultlocation.py @@ -19,6 +19,9 @@ """This module contains the classes that represent Telegram InlineQueryResultLocation.""" from telegram import InlineQueryResult +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import ReplyMarkup, InputMessageContent class InlineQueryResultLocation(InlineQueryResult): @@ -62,17 +65,17 @@ class InlineQueryResultLocation(InlineQueryResult): """ def __init__(self, - id, - latitude, - longitude, - title, - live_period=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - **kwargs): + id: str, + latitude: float, + longitude: float, + title: str, + live_period: int = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + **kwargs: Any): # Required super().__init__('location', id) self.latitude = latitude diff --git a/telegram/inline/inlinequeryresultmpeg4gif.py b/telegram/inline/inlinequeryresultmpeg4gif.py index 5e101fd63..eb8e22f35 100644 --- a/telegram/inline/inlinequeryresultmpeg4gif.py +++ b/telegram/inline/inlinequeryresultmpeg4gif.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultMpeg4Gif(InlineQueryResult): @@ -74,19 +77,19 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): """ def __init__(self, - id, - mpeg4_url, - thumb_url, - mpeg4_width=None, - mpeg4_height=None, - title=None, - caption=None, - reply_markup=None, - input_message_content=None, - mpeg4_duration=None, - parse_mode=DEFAULT_NONE, - thumb_mime_type=None, - **kwargs): + id: str, + mpeg4_url: str, + thumb_url: str, + mpeg4_width: int = None, + mpeg4_height: int = None, + title: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + mpeg4_duration: int = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + thumb_mime_type: str = None, + **kwargs: Any): # Required super().__init__('mpeg4_gif', id) diff --git a/telegram/inline/inlinequeryresultphoto.py b/telegram/inline/inlinequeryresultphoto.py index c51fbda4b..6c9c58aa0 100644 --- a/telegram/inline/inlinequeryresultphoto.py +++ b/telegram/inline/inlinequeryresultphoto.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultPhoto.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultPhoto(InlineQueryResult): @@ -71,18 +74,18 @@ class InlineQueryResultPhoto(InlineQueryResult): """ def __init__(self, - id, - photo_url, - thumb_url, - photo_width=None, - photo_height=None, - title=None, - description=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + photo_url: str, + thumb_url: str, + photo_width: int = None, + photo_height: int = None, + title: str = None, + description: str = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('photo', id) self.photo_url = photo_url diff --git a/telegram/inline/inlinequeryresultvenue.py b/telegram/inline/inlinequeryresultvenue.py index 296db4123..da54c2b81 100644 --- a/telegram/inline/inlinequeryresultvenue.py +++ b/telegram/inline/inlinequeryresultvenue.py @@ -19,6 +19,9 @@ """This module contains the classes that represent Telegram InlineQueryResultVenue.""" from telegram import InlineQueryResult +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import ReplyMarkup, InputMessageContent class InlineQueryResultVenue(InlineQueryResult): @@ -68,19 +71,19 @@ class InlineQueryResultVenue(InlineQueryResult): """ def __init__(self, - id, - latitude, - longitude, - title, - address, - foursquare_id=None, - foursquare_type=None, - reply_markup=None, - input_message_content=None, - thumb_url=None, - thumb_width=None, - thumb_height=None, - **kwargs): + id: str, + latitude: float, + longitude: float, + title: str, + address: str, + foursquare_id: str = None, + foursquare_type: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + thumb_url: str = None, + thumb_width: int = None, + thumb_height: int = None, + **kwargs: Any): # Required super().__init__('venue', id) diff --git a/telegram/inline/inlinequeryresultvideo.py b/telegram/inline/inlinequeryresultvideo.py index 5b1daa0b2..f2856a655 100644 --- a/telegram/inline/inlinequeryresultvideo.py +++ b/telegram/inline/inlinequeryresultvideo.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultVideo.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultVideo(InlineQueryResult): @@ -81,20 +84,20 @@ class InlineQueryResultVideo(InlineQueryResult): """ def __init__(self, - id, - video_url, - mime_type, - thumb_url, - title, - caption=None, - video_width=None, - video_height=None, - video_duration=None, - description=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + video_url: str, + mime_type: str, + thumb_url: str, + title: str, + caption: str = None, + video_width: int = None, + video_height: int = None, + video_duration: int = None, + description: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('video', id) diff --git a/telegram/inline/inlinequeryresultvoice.py b/telegram/inline/inlinequeryresultvoice.py index 97a4acfc3..795f7be00 100644 --- a/telegram/inline/inlinequeryresultvoice.py +++ b/telegram/inline/inlinequeryresultvoice.py @@ -19,7 +19,10 @@ """This module contains the classes that represent Telegram InlineQueryResultVoice.""" from telegram import InlineQueryResult -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultVoice(InlineQueryResult): @@ -62,15 +65,15 @@ class InlineQueryResultVoice(InlineQueryResult): """ def __init__(self, - id, - voice_url, - title, - voice_duration=None, - caption=None, - reply_markup=None, - input_message_content=None, - parse_mode=DEFAULT_NONE, - **kwargs): + id: str, + voice_url: str, + title: str, + voice_duration: int = None, + caption: str = None, + reply_markup: 'ReplyMarkup' = None, + input_message_content: 'InputMessageContent' = None, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required super().__init__('voice', id) diff --git a/telegram/inline/inputcontactmessagecontent.py b/telegram/inline/inputcontactmessagecontent.py index efcd1e3ad..a5530d0b8 100644 --- a/telegram/inline/inputcontactmessagecontent.py +++ b/telegram/inline/inputcontactmessagecontent.py @@ -19,6 +19,7 @@ """This module contains the classes that represent Telegram InputContactMessageContent.""" from telegram import InputMessageContent +from typing import Any class InputContactMessageContent(InputMessageContent): @@ -44,7 +45,12 @@ class InputContactMessageContent(InputMessageContent): """ - def __init__(self, phone_number, first_name, last_name=None, vcard=None, **kwargs): + def __init__(self, + phone_number: str, + first_name: str, + last_name: str = None, + vcard: str = None, + **kwargs: Any): # Required self.phone_number = phone_number self.first_name = first_name diff --git a/telegram/inline/inputlocationmessagecontent.py b/telegram/inline/inputlocationmessagecontent.py index a1b5639d7..b29713fdf 100644 --- a/telegram/inline/inputlocationmessagecontent.py +++ b/telegram/inline/inputlocationmessagecontent.py @@ -19,6 +19,7 @@ """This module contains the classes that represent Telegram InputLocationMessageContent.""" from telegram import InputMessageContent +from typing import Any class InputLocationMessageContent(InputMessageContent): @@ -43,7 +44,7 @@ class InputLocationMessageContent(InputMessageContent): """ - def __init__(self, latitude, longitude, live_period=None, **kwargs): + def __init__(self, latitude: float, longitude: float, live_period: int = None, **kwargs: Any): # Required self.latitude = latitude self.longitude = longitude diff --git a/telegram/inline/inputmessagecontent.py b/telegram/inline/inputmessagecontent.py index d045306e5..fd5b30817 100644 --- a/telegram/inline/inputmessagecontent.py +++ b/telegram/inline/inputmessagecontent.py @@ -30,9 +30,9 @@ class InputMessageContent(TelegramObject): """ @property - def _has_parse_mode(self): + def _has_parse_mode(self) -> bool: return hasattr(self, 'parse_mode') @property - def _has_disable_web_page_preview(self): + def _has_disable_web_page_preview(self) -> bool: return hasattr(self, 'disable_web_page_preview') diff --git a/telegram/inline/inputtextmessagecontent.py b/telegram/inline/inputtextmessagecontent.py index f7645e59a..79236d32d 100644 --- a/telegram/inline/inputtextmessagecontent.py +++ b/telegram/inline/inputtextmessagecontent.py @@ -19,7 +19,8 @@ """This module contains the classes that represent Telegram InputTextMessageContent.""" from telegram import InputMessageContent -from telegram.utils.helpers import DEFAULT_NONE +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from typing import Any, Union class InputTextMessageContent(InputMessageContent): @@ -51,10 +52,10 @@ class InputTextMessageContent(InputMessageContent): """ def __init__(self, - message_text, - parse_mode=DEFAULT_NONE, - disable_web_page_preview=DEFAULT_NONE, - **kwargs): + message_text: str, + parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, + disable_web_page_preview: Union[bool, DefaultValue] = DEFAULT_NONE, + **kwargs: Any): # Required self.message_text = message_text # Optionals diff --git a/telegram/inline/inputvenuemessagecontent.py b/telegram/inline/inputvenuemessagecontent.py index bcd67dd1e..e4b3fad5d 100644 --- a/telegram/inline/inputvenuemessagecontent.py +++ b/telegram/inline/inputvenuemessagecontent.py @@ -19,6 +19,7 @@ """This module contains the classes that represent Telegram InputVenueMessageContent.""" from telegram import InputMessageContent +from typing import Any class InputVenueMessageContent(InputMessageContent): @@ -51,8 +52,14 @@ class InputVenueMessageContent(InputMessageContent): """ - def __init__(self, latitude, longitude, title, address, foursquare_id=None, - foursquare_type=None, **kwargs): + def __init__(self, + latitude: float, + longitude: float, + title: str, + address: str, + foursquare_id: str = None, + foursquare_type: str = None, + **kwargs: Any): # Required self.latitude = latitude self.longitude = longitude diff --git a/telegram/keyboardbutton.py b/telegram/keyboardbutton.py index de6928dde..d0fef2b06 100644 --- a/telegram/keyboardbutton.py +++ b/telegram/keyboardbutton.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram KeyboardButton.""" from telegram import TelegramObject +from typing import Any class KeyboardButton(TelegramObject): @@ -59,8 +60,12 @@ class KeyboardButton(TelegramObject): """ - def __init__(self, text, request_contact=None, request_location=None, request_poll=None, - **kwargs): + def __init__(self, + text: str, + request_contact: bool = None, + request_location: bool = None, + request_poll: bool = None, + **kwargs: Any): # Required self.text = text # Optionals diff --git a/telegram/keyboardbuttonpolltype.py b/telegram/keyboardbuttonpolltype.py index 46e2089cd..3a7ca84bc 100644 --- a/telegram/keyboardbuttonpolltype.py +++ b/telegram/keyboardbuttonpolltype.py @@ -19,6 +19,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a type of a Telegram Poll.""" from telegram import TelegramObject +from typing import Any class KeyboardButtonPollType(TelegramObject): @@ -34,7 +35,7 @@ class KeyboardButtonPollType(TelegramObject): passed, only regular polls will be allowed. Otherwise, the user will be allowed to create a poll of any type. """ - def __init__(self, type=None): + def __init__(self, type: str = None, **kwargs: Any): self.type = type self._id_attrs = (self.type,) diff --git a/telegram/loginurl.py b/telegram/loginurl.py index 844d61aba..01798761e 100644 --- a/telegram/loginurl.py +++ b/telegram/loginurl.py @@ -19,6 +19,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram LoginUrl.""" from telegram import TelegramObject +from typing import Any class LoginUrl(TelegramObject): @@ -66,7 +67,12 @@ class LoginUrl(TelegramObject): `Checking authorization `_ """ - def __init__(self, url, forward_text=None, bot_username=None, request_write_access=None): + def __init__(self, + url: str, + forward_text: bool = None, + bot_username: str = None, + request_write_access: bool = None, + **kwargs: Any): # Required self.url = url # Optional diff --git a/telegram/message.py b/telegram/message.py index e48f07923..4149fc2c4 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -19,6 +19,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Message.""" import sys +import datetime from html import escape from telegram import (Animation, Audio, Contact, Document, Chat, Location, PhotoSize, Sticker, @@ -27,6 +28,11 @@ from telegram import (Animation, Audio, Contact, Document, Chat, Location, Photo from telegram import ParseMode from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp +from telegram.utils.types import JSONDict +from typing import Any, List, Dict, Optional, Union, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, InputMedia, GameHighScore + _UNDEFINED = object() @@ -203,7 +209,7 @@ class Message(TelegramObject): programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. - pinned_message (:class:`telegram.message`, optional): Specified message was pinned. Note + pinned_message (:class:`telegram.Message`, optional): Specified message was pinned. Note that the Message object in this field will not contain further :attr:`reply_to_message` fields even if it is itself a reply. invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, @@ -239,57 +245,57 @@ class Message(TelegramObject): 'passport_data'] + ATTACHMENT_TYPES def __init__(self, - message_id, - from_user, - date, - chat, - forward_from=None, - forward_from_chat=None, - forward_from_message_id=None, - forward_date=None, - reply_to_message=None, - edit_date=None, - text=None, - entities=None, - caption_entities=None, - audio=None, - document=None, - game=None, - photo=None, - sticker=None, - video=None, - voice=None, - video_note=None, - new_chat_members=None, - caption=None, - contact=None, - location=None, - venue=None, - left_chat_member=None, - new_chat_title=None, - new_chat_photo=None, - delete_chat_photo=False, - group_chat_created=False, - supergroup_chat_created=False, - channel_chat_created=False, - migrate_to_chat_id=None, - migrate_from_chat_id=None, - pinned_message=None, - invoice=None, - successful_payment=None, - forward_signature=None, - author_signature=None, - media_group_id=None, - connected_website=None, - animation=None, - passport_data=None, - poll=None, - forward_sender_name=None, - reply_markup=None, - bot=None, - dice=None, - via_bot=None, - **kwargs): + message_id: int, + date: datetime.datetime, + chat: Chat, + from_user: User = None, + forward_from: User = None, + forward_from_chat: Chat = None, + forward_from_message_id: int = None, + forward_date: datetime.datetime = None, + reply_to_message: 'Message' = None, + edit_date: datetime.datetime = None, + text: str = None, + entities: List[MessageEntity] = None, + caption_entities: List[MessageEntity] = None, + audio: Audio = None, + document: Document = None, + game: Game = None, + photo: List[PhotoSize] = None, + sticker: Sticker = None, + video: Video = None, + voice: Voice = None, + video_note: VideoNote = None, + new_chat_members: List[User] = None, + caption: str = None, + contact: Contact = None, + location: Location = None, + venue: Venue = None, + left_chat_member: User = None, + new_chat_title: str = None, + new_chat_photo: List[PhotoSize] = None, + delete_chat_photo: bool = False, + group_chat_created: bool = False, + supergroup_chat_created: bool = False, + channel_chat_created: bool = False, + migrate_to_chat_id: int = None, + migrate_from_chat_id: int = None, + pinned_message: 'Message' = None, + invoice: Invoice = None, + successful_payment: SuccessfulPayment = None, + forward_signature: str = None, + author_signature: str = None, + media_group_id: str = None, + connected_website: str = None, + animation: Animation = None, + passport_data: PassportData = None, + poll: Poll = None, + forward_sender_name: str = None, + reply_markup: InlineKeyboardMarkup = None, + bot: 'Bot' = None, + dice: Dice = None, + via_bot: User = None, + **kwargs: Any): # Required self.message_id = int(message_id) self.from_user = from_user @@ -346,12 +352,12 @@ class Message(TelegramObject): self._id_attrs = (self.message_id, self.chat) @property - def chat_id(self): + def chat_id(self) -> int: """:obj:`int`: Shortcut for :attr:`telegram.Chat.id` for :attr:`chat`.""" return self.chat.id @property - def link(self): + def link(self) -> Optional[str]: """:obj:`str`: Convenience property. If the chat of the message is not a private chat or normal group, returns a t.me link of the message.""" if self.chat.type not in [Chat.PRIVATE, Chat.GROUP]: @@ -364,12 +370,12 @@ class Message(TelegramObject): return None @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> 'Message': + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['from_user'] = User.de_json(data.get('from'), bot) data['date'] = from_timestamp(data['date']) data['chat'] = Chat.de_json(data.get('chat'), bot) @@ -407,7 +413,9 @@ class Message(TelegramObject): return cls(bot=bot, **data) @property - def effective_attachment(self): + def effective_attachment(self) -> Union[Contact, Document, Animation, Game, Invoice, Location, + List[PhotoSize], Sticker, SuccessfulPayment, Venue, + Video, VideoNote, Voice, None]: """ :class:`telegram.Audio` or :class:`telegram.Contact` @@ -427,7 +435,7 @@ class Message(TelegramObject): """ if self._effective_attachment is not _UNDEFINED: - return self._effective_attachment + return self._effective_attachment # type: ignore for i in Message.ATTACHMENT_TYPES: if getattr(self, i, None): @@ -436,15 +444,15 @@ class Message(TelegramObject): else: self._effective_attachment = None - return self._effective_attachment + return self._effective_attachment # type: ignore - def __getitem__(self, item): + def __getitem__(self, item: str) -> Any: if item in self.__dict__.keys(): return self.__dict__[item] elif item == 'chat_id': return self.chat.id - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() # Required @@ -467,7 +475,7 @@ class Message(TelegramObject): return data - def _quote(self, kwargs): + def _quote(self, kwargs: JSONDict) -> None: """Modify kwargs for replying with or without quoting.""" if 'reply_to_message_id' in kwargs: if 'quote' in kwargs: @@ -487,7 +495,7 @@ class Message(TelegramObject): if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote: kwargs['reply_to_message_id'] = self.message_id - def reply_text(self, *args, **kwargs): + def reply_text(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_message(update.message.chat_id, *args, **kwargs) @@ -505,7 +513,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_message(self.chat_id, *args, **kwargs) - def reply_markdown(self, *args, **kwargs): + def reply_markdown(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_message(update.message.chat_id, parse_mode=ParseMode.MARKDOWN, *args, @@ -533,7 +541,7 @@ class Message(TelegramObject): return self.bot.send_message(self.chat_id, *args, **kwargs) - def reply_markdown_v2(self, *args, **kwargs): + def reply_markdown_v2(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_message(update.message.chat_id, parse_mode=ParseMode.MARKDOWN_V2, *args, @@ -557,7 +565,7 @@ class Message(TelegramObject): return self.bot.send_message(self.chat_id, *args, **kwargs) - def reply_html(self, *args, **kwargs): + def reply_html(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_message(update.message.chat_id, parse_mode=ParseMode.HTML, *args, **kwargs) @@ -580,7 +588,7 @@ class Message(TelegramObject): return self.bot.send_message(self.chat_id, *args, **kwargs) - def reply_media_group(self, *args, **kwargs): + def reply_media_group(self, *args: Any, **kwargs: Any) -> List[Optional['Message']]: """Shortcut for:: bot.send_media_group(update.message.chat_id, *args, **kwargs) @@ -600,7 +608,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_media_group(self.chat_id, *args, **kwargs) - def reply_photo(self, *args, **kwargs): + def reply_photo(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_photo(update.message.chat_id, *args, **kwargs) @@ -618,7 +626,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_photo(self.chat_id, *args, **kwargs) - def reply_audio(self, *args, **kwargs): + def reply_audio(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_audio(update.message.chat_id, *args, **kwargs) @@ -636,7 +644,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_audio(self.chat_id, *args, **kwargs) - def reply_document(self, *args, **kwargs): + def reply_document(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_document(update.message.chat_id, *args, **kwargs) @@ -654,7 +662,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_document(self.chat_id, *args, **kwargs) - def reply_animation(self, *args, **kwargs): + def reply_animation(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_animation(update.message.chat_id, *args, **kwargs) @@ -672,7 +680,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_animation(self.chat_id, *args, **kwargs) - def reply_sticker(self, *args, **kwargs): + def reply_sticker(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_sticker(update.message.chat_id, *args, **kwargs) @@ -690,7 +698,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_sticker(self.chat_id, *args, **kwargs) - def reply_video(self, *args, **kwargs): + def reply_video(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_video(update.message.chat_id, *args, **kwargs) @@ -708,7 +716,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_video(self.chat_id, *args, **kwargs) - def reply_video_note(self, *args, **kwargs): + def reply_video_note(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_video_note(update.message.chat_id, *args, **kwargs) @@ -726,7 +734,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_video_note(self.chat_id, *args, **kwargs) - def reply_voice(self, *args, **kwargs): + def reply_voice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_voice(update.message.chat_id, *args, **kwargs) @@ -744,7 +752,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_voice(self.chat_id, *args, **kwargs) - def reply_location(self, *args, **kwargs): + def reply_location(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_location(update.message.chat_id, *args, **kwargs) @@ -762,7 +770,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_location(self.chat_id, *args, **kwargs) - def reply_venue(self, *args, **kwargs): + def reply_venue(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_venue(update.message.chat_id, *args, **kwargs) @@ -780,7 +788,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_venue(self.chat_id, *args, **kwargs) - def reply_contact(self, *args, **kwargs): + def reply_contact(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_contact(update.message.chat_id, *args, **kwargs) @@ -798,7 +806,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_contact(self.chat_id, *args, **kwargs) - def reply_poll(self, *args, **kwargs): + def reply_poll(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_poll(update.message.chat_id, *args, **kwargs) @@ -816,7 +824,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_poll(self.chat_id, *args, **kwargs) - def reply_dice(self, *args, **kwargs): + def reply_dice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_dice(update.message.chat_id, *args, **kwargs) @@ -834,7 +842,7 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_dice(self.chat_id, *args, **kwargs) - def forward(self, chat_id, *args, **kwargs): + def forward(self, chat_id: int, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.forward_message(chat_id=chat_id, @@ -854,7 +862,7 @@ class Message(TelegramObject): *args, **kwargs) - def edit_text(self, *args, **kwargs): + def edit_text(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_text(chat_id=message.chat_id, @@ -868,13 +876,14 @@ class Message(TelegramObject): behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, instance representing the edited message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_text( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def edit_caption(self, *args, **kwargs): + def edit_caption(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_caption(chat_id=message.chat_id, @@ -888,34 +897,35 @@ class Message(TelegramObject): behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, instance representing the edited message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_caption( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def edit_media(self, media, *args, **kwargs): + def edit_media(self, media: 'InputMedia', *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_media(chat_id=message.chat_id, - message_id=message.message_id, - *args, - **kwargs) + message_id=message.message_id, + *args, + **kwargs) Note: - You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + You can only edit messages that the bot sent itself(i.e. of the ``bot.send_*`` family of methods) or channel posts, if the bot is an admin in that channel. However, this behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, instance representing the edited - message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_media( chat_id=self.chat_id, message_id=self.message_id, media=media, *args, **kwargs) - def edit_reply_markup(self, *args, **kwargs): + def edit_reply_markup(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_reply_markup(chat_id=message.chat_id, @@ -929,12 +939,13 @@ class Message(TelegramObject): behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, instance representing the edited message. + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_reply_markup( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def edit_live_location(self, *args, **kwargs): + def edit_live_location(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: bot.edit_message_live_location(chat_id=message.chat_id, @@ -954,7 +965,7 @@ class Message(TelegramObject): return self.bot.edit_message_live_location( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def stop_live_location(self, *args, **kwargs): + def stop_live_location(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: bot.stop_message_live_location(chat_id=message.chat_id, @@ -974,7 +985,7 @@ class Message(TelegramObject): return self.bot.stop_message_live_location( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def set_game_score(self, *args, **kwargs): + def set_game_score(self, *args: Any, **kwargs: Any) -> Union['Message', bool]: """Shortcut for:: bot.set_game_score(chat_id=message.chat_id, @@ -994,7 +1005,7 @@ class Message(TelegramObject): return self.bot.set_game_score( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def get_game_high_scores(self, *args, **kwargs): + def get_game_high_scores(self, *args: Any, **kwargs: Any) -> List['GameHighScore']: """Shortcut for:: bot.get_game_high_scores(chat_id=message.chat_id, @@ -1008,13 +1019,12 @@ class Message(TelegramObject): behaviour is undocumented and might be changed by Telegram. Returns: - :class:`telegram.Message`: On success, if edited message is sent by the bot, the - edited Message is returned, otherwise :obj:`True` is returned. + List[:class:`telegram.GameHighScore`] """ return self.bot.get_game_high_scores( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def delete(self, *args, **kwargs): + def delete(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.delete_message(chat_id=message.chat_id, @@ -1029,7 +1039,7 @@ class Message(TelegramObject): return self.bot.delete_message( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def stop_poll(self, *args, **kwargs): + def stop_poll(self, *args: Any, **kwargs: Any) -> Poll: """Shortcut for:: bot.stop_poll(chat_id=message.chat_id, @@ -1045,7 +1055,7 @@ class Message(TelegramObject): return self.bot.stop_poll( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def pin(self, *args, **kwargs): + def pin(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.pin_chat_message(chat_id=message.chat_id, @@ -1060,7 +1070,7 @@ class Message(TelegramObject): return self.bot.pin_chat_message( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) - def parse_entity(self, entity): + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. Note: @@ -1075,7 +1085,13 @@ class Message(TelegramObject): Returns: :obj:`str`: The text of the given entity. + Raises: + RuntimeError: If the message has no text. + """ + if not self.text: + raise RuntimeError("This Message has no 'text'.") + # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xffff: return self.text[entity.offset:entity.offset + entity.length] @@ -1085,7 +1101,7 @@ class Message(TelegramObject): return entity_text.decode('utf-16-le') - def parse_caption_entity(self, entity): + def parse_caption_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. Note: @@ -1100,7 +1116,13 @@ class Message(TelegramObject): Returns: :obj:`str`: The text of the given entity. + Raises: + RuntimeError: If the message has no caption. + """ + if not self.caption: + raise RuntimeError("This Message has no 'caption'.") + # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xffff: return self.caption[entity.offset:entity.offset + entity.length] @@ -1110,7 +1132,7 @@ class Message(TelegramObject): return entity_text.decode('utf-16-le') - def parse_entities(self, types=None): + def parse_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message filtered by their @@ -1138,10 +1160,10 @@ class Message(TelegramObject): return { entity: self.parse_entity(entity) - for entity in self.entities if entity.type in types + for entity in (self.entities or []) if entity.type in types } - def parse_caption_entities(self, types=None): + def parse_caption_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message's caption filtered by their @@ -1169,16 +1191,19 @@ class Message(TelegramObject): return { entity: self.parse_caption_entity(entity) - for entity in self.caption_entities if entity.type in types + for entity in (self.caption_entities or []) if entity.type in types } @staticmethod - def _parse_html(message_text, entities, urled=False, offset=0): + def _parse_html(message_text: Optional[str], + entities: Dict[MessageEntity, str], + urled: bool = False, + offset: int = 0) -> Optional[str]: if message_text is None: return None if not sys.maxunicode == 0xffff: - message_text = message_text.encode('utf-16-le') + message_text = message_text.encode('utf-16-le') # type: ignore html_text = '' last_offset = 0 @@ -1232,15 +1257,16 @@ class Message(TelegramObject): html_text += escape(message_text[last_offset:entity.offset - offset]) + insert else: - html_text += escape(message_text[last_offset * 2:(entity.offset - - offset) * 2] - .decode('utf-16-le')) + insert + html_text += escape(message_text[ # type: ignore + last_offset * 2:(entity.offset - offset) * 2].decode('utf-16-le') + ) + insert else: if sys.maxunicode == 0xffff: html_text += message_text[last_offset:entity.offset - offset] + insert else: - html_text += message_text[last_offset * 2:(entity.offset - - offset) * 2].decode('utf-16-le') + insert + html_text += message_text[ # type: ignore + last_offset * 2:(entity.offset - offset) * 2 + ].decode('utf-16-le') + insert last_offset = entity.offset - offset + entity.length @@ -1248,17 +1274,18 @@ class Message(TelegramObject): if sys.maxunicode == 0xffff: html_text += escape(message_text[last_offset:]) else: - html_text += escape(message_text[last_offset * 2:].decode('utf-16-le')) + html_text += escape( + message_text[last_offset * 2:].decode('utf-16-le')) # type: ignore else: if sys.maxunicode == 0xffff: html_text += message_text[last_offset:] else: - html_text += message_text[last_offset * 2:].decode('utf-16-le') + html_text += message_text[last_offset * 2:].decode('utf-16-le') # type: ignore return html_text @property - def text_html(self): + def text_html(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message. Use this if you want to retrieve the message text with the entities formatted as HTML in @@ -1271,7 +1298,7 @@ class Message(TelegramObject): return self._parse_html(self.text, self.parse_entities(), urled=False) @property - def text_html_urled(self): + def text_html_urled(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message. Use this if you want to retrieve the message text with the entities formatted as HTML. @@ -1284,7 +1311,7 @@ class Message(TelegramObject): return self._parse_html(self.text, self.parse_entities(), urled=True) @property - def caption_html(self): + def caption_html(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message's caption. @@ -1298,7 +1325,7 @@ class Message(TelegramObject): return self._parse_html(self.caption, self.parse_caption_entities(), urled=False) @property - def caption_html_urled(self): + def caption_html_urled(self) -> str: """Creates an HTML-formatted string from the markup entities found in the message's caption. @@ -1312,14 +1339,18 @@ class Message(TelegramObject): return self._parse_html(self.caption, self.parse_caption_entities(), urled=True) @staticmethod - def _parse_markdown(message_text, entities, urled=False, version=1, offset=0): + def _parse_markdown(message_text: Optional[str], + entities: Dict[MessageEntity, str], + urled: bool = False, + version: int = 1, + offset: int = 0) -> Optional[str]: version = int(version) if message_text is None: return None if not sys.maxunicode == 0xffff: - message_text = message_text.encode('utf-16-le') + message_text = message_text.encode('utf-16-le') # type: ignore markdown_text = '' last_offset = 0 @@ -1404,16 +1435,18 @@ class Message(TelegramObject): - offset], version=version) + insert else: - markdown_text += escape_markdown(message_text[last_offset * 2: - (entity.offset - offset) * 2] - .decode('utf-16-le'), - version=version) + insert + markdown_text += escape_markdown( + message_text[ # type: ignore + last_offset * 2: (entity.offset - offset) * 2 + ].decode('utf-16-le'), + version=version) + insert else: if sys.maxunicode == 0xffff: markdown_text += message_text[last_offset:entity.offset - offset] + insert else: - markdown_text += message_text[last_offset * 2:(entity.offset - - offset) * 2].decode('utf-16-le') + insert + markdown_text += message_text[ # type: ignore + last_offset * 2:(entity.offset - offset) * 2 + ].decode('utf-16-le') + insert last_offset = entity.offset - offset + entity.length @@ -1421,18 +1454,19 @@ class Message(TelegramObject): if sys.maxunicode == 0xffff: markdown_text += escape_markdown(message_text[last_offset:], version=version) else: - markdown_text += escape_markdown(message_text[last_offset * 2:] - .decode('utf-16-le'), version=version) + markdown_text += escape_markdown( + message_text[last_offset * 2:] .decode('utf-16-le'), # type: ignore + version=version) else: if sys.maxunicode == 0xffff: markdown_text += message_text[last_offset:] else: - markdown_text += message_text[last_offset * 2:].decode('utf-16-le') + markdown_text += message_text[last_offset * 2:].decode('utf-16-le') # type: ignore return markdown_text @property - def text_markdown(self): + def text_markdown(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message using :class:`telegram.ParseMode.MARKDOWN`. @@ -1450,7 +1484,7 @@ class Message(TelegramObject): return self._parse_markdown(self.text, self.parse_entities(), urled=False) @property - def text_markdown_v2(self): + def text_markdown_v2(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message using :class:`telegram.ParseMode.MARKDOWN_V2`. @@ -1464,7 +1498,7 @@ class Message(TelegramObject): return self._parse_markdown(self.text, self.parse_entities(), urled=False, version=2) @property - def text_markdown_urled(self): + def text_markdown_urled(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message using :class:`telegram.ParseMode.MARKDOWN`. @@ -1482,7 +1516,7 @@ class Message(TelegramObject): return self._parse_markdown(self.text, self.parse_entities(), urled=True) @property - def text_markdown_v2_urled(self): + def text_markdown_v2_urled(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message using :class:`telegram.ParseMode.MARKDOWN_V2`. @@ -1496,7 +1530,7 @@ class Message(TelegramObject): return self._parse_markdown(self.text, self.parse_entities(), urled=True, version=2) @property - def caption_markdown(self): + def caption_markdown(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message's caption using :class:`telegram.ParseMode.MARKDOWN`. @@ -1514,7 +1548,7 @@ class Message(TelegramObject): return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=False) @property - def caption_markdown_v2(self): + def caption_markdown_v2(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message's caption using :class:`telegram.ParseMode.MARKDOWN_V2`. @@ -1529,7 +1563,7 @@ class Message(TelegramObject): urled=False, version=2) @property - def caption_markdown_urled(self): + def caption_markdown_urled(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message's caption using :class:`telegram.ParseMode.MARKDOWN`. @@ -1547,7 +1581,7 @@ class Message(TelegramObject): return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=True) @property - def caption_markdown_v2_urled(self): + def caption_markdown_v2_urled(self) -> str: """Creates an Markdown-formatted string from the markup entities found in the message's caption using :class:`telegram.ParseMode.MARKDOWN_V2`. diff --git a/telegram/messageentity.py b/telegram/messageentity.py index f76068bb5..ff1970b88 100644 --- a/telegram/messageentity.py +++ b/telegram/messageentity.py @@ -19,6 +19,11 @@ """This module contains an object that represents a Telegram MessageEntity.""" from telegram import User, TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, List, TYPE_CHECKING + +if TYPE_CHECKING: + from telegram import Bot class MessageEntity(TelegramObject): @@ -53,7 +58,14 @@ class MessageEntity(TelegramObject): """ - def __init__(self, type, offset, length, url=None, user=None, language=None, **kwargs): + def __init__(self, + type: str, + offset: int, + length: int, + url: str = None, + user: User = None, + language: str = None, + **kwargs: Any): # Required self.type = type self.offset = offset @@ -66,8 +78,8 @@ class MessageEntity(TelegramObject): self._id_attrs = (self.type, self.offset, self.length) @classmethod - def de_json(cls, data, bot): - data = super().de_json(data, bot) + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MessageEntity']: + data = cls.parse_data(data) if not data: return None @@ -76,48 +88,37 @@ class MessageEntity(TelegramObject): return cls(**data) - @classmethod - def de_list(cls, data, bot): - if not data: - return list() - - entities = list() - for entity in data: - entities.append(cls.de_json(entity, bot)) - - return entities - - MENTION = 'mention' + MENTION: str = 'mention' """:obj:`str`: 'mention'""" - HASHTAG = 'hashtag' + HASHTAG: str = 'hashtag' """:obj:`str`: 'hashtag'""" - CASHTAG = 'cashtag' + CASHTAG: str = 'cashtag' """:obj:`str`: 'cashtag'""" - PHONE_NUMBER = 'phone_number' + PHONE_NUMBER: str = 'phone_number' """:obj:`str`: 'phone_number'""" - BOT_COMMAND = 'bot_command' + BOT_COMMAND: str = 'bot_command' """:obj:`str`: 'bot_command'""" - URL = 'url' + URL: str = 'url' """:obj:`str`: 'url'""" - EMAIL = 'email' + EMAIL: str = 'email' """:obj:`str`: 'email'""" - BOLD = 'bold' + BOLD: str = 'bold' """:obj:`str`: 'bold'""" - ITALIC = 'italic' + ITALIC: str = 'italic' """:obj:`str`: 'italic'""" - CODE = 'code' + CODE: str = 'code' """:obj:`str`: 'code'""" - PRE = 'pre' + PRE: str = 'pre' """:obj:`str`: 'pre'""" - TEXT_LINK = 'text_link' + TEXT_LINK: str = 'text_link' """:obj:`str`: 'text_link'""" - TEXT_MENTION = 'text_mention' + TEXT_MENTION: str = 'text_mention' """:obj:`str`: 'text_mention'""" - UNDERLINE = 'underline' + UNDERLINE: str = 'underline' """:obj:`str`: 'underline'""" - STRIKETHROUGH = 'strikethrough' + STRIKETHROUGH: str = 'strikethrough' """:obj:`str`: 'strikethrough'""" - ALL_TYPES = [ + ALL_TYPES: List[str] = [ MENTION, HASHTAG, CASHTAG, PHONE_NUMBER, BOT_COMMAND, URL, EMAIL, BOLD, ITALIC, CODE, PRE, TEXT_LINK, TEXT_MENTION, UNDERLINE, STRIKETHROUGH ] diff --git a/telegram/parsemode.py b/telegram/parsemode.py index 96ee5a111..ee8ad8ef5 100644 --- a/telegram/parsemode.py +++ b/telegram/parsemode.py @@ -23,14 +23,14 @@ class ParseMode: """This object represents a Telegram Message Parse Modes.""" - MARKDOWN = 'Markdown' + MARKDOWN: str = 'Markdown' """:obj:`str`: 'Markdown' Note: :attr:`MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. You should use :attr:`MARKDOWN_V2` instead. """ - MARKDOWN_V2 = 'MarkdownV2' + MARKDOWN_V2: str = 'MarkdownV2' """:obj:`str`: 'MarkdownV2'""" - HTML = 'HTML' + HTML: str = 'HTML' """:obj:`str`: 'HTML'""" diff --git a/telegram/passport/credentials.py b/telegram/passport/credentials.py index 6ab1795ba..b48cb7c19 100644 --- a/telegram/passport/credentials.py +++ b/telegram/passport/credentials.py @@ -19,7 +19,7 @@ try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] from base64 import b64decode from cryptography.hazmat.backends import default_backend @@ -30,6 +30,11 @@ from cryptography.hazmat.primitives.ciphers.modes import CBC from cryptography.hazmat.primitives.hashes import SHA512, SHA256, Hash, SHA1 from telegram import TelegramObject, TelegramError +from telegram.utils.types import JSONDict +from typing import Union, Any, Optional, TYPE_CHECKING, List, no_type_check, Tuple + +if TYPE_CHECKING: + from telegram import Bot class TelegramDecryptionError(TelegramError): @@ -37,14 +42,15 @@ class TelegramDecryptionError(TelegramError): Something went wrong with decryption. """ - def __init__(self, message): + def __init__(self, message: Union[str, Exception]): super().__init__("TelegramDecryptionError: {}".format(message)) - self._msg = message + self._msg = str(message) - def __reduce__(self): + def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self._msg,) +@no_type_check def decrypt(secret, hash, data): """ Decrypt per telegram docs at https://core.telegram.org/passport. @@ -88,6 +94,7 @@ def decrypt(secret, hash, data): return data[data[0]:] +@no_type_check def decrypt_json(secret, hash, data): """Decrypts data using secret and hash and then decodes utf-8 string and loads json""" return json.loads(decrypt(secret, hash, data).decode('utf-8')) @@ -122,7 +129,12 @@ class EncryptedCredentials(TelegramObject): """ - def __init__(self, data, hash, secret, bot=None, **kwargs): + def __init__(self, + data: str, + hash: str, + secret: str, + bot: 'Bot' = None, + **kwargs: Any): # Required self.data = data self.hash = hash @@ -132,19 +144,10 @@ class EncryptedCredentials(TelegramObject): self.bot = bot self._decrypted_secret = None - self._decrypted_data = None - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - data = super().de_json(data, bot) - - return cls(bot=bot, **data) + self._decrypted_data: Optional['Credentials'] = None @property - def decrypted_secret(self): + def decrypted_secret(self) -> str: """ :obj:`str`: Lazily decrypt and return secret. @@ -171,7 +174,7 @@ class EncryptedCredentials(TelegramObject): return self._decrypted_secret @property - def decrypted_data(self): + def decrypted_data(self) -> 'Credentials': """ :class:`telegram.Credentials`: Lazily decrypt and return credentials data. This object also contains the user specified nonce as @@ -196,7 +199,7 @@ class Credentials(TelegramObject): nonce (:obj:`str`): Bot-specified nonce """ - def __init__(self, secure_data, nonce, bot=None, **kwargs): + def __init__(self, secure_data: 'SecureData', nonce: str, bot: 'Bot' = None, **kwargs: Any): # Required self.secure_data = secure_data self.nonce = nonce @@ -204,7 +207,9 @@ class Credentials(TelegramObject): self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Credentials']: + data = cls.parse_data(data) + if not data: return None @@ -242,19 +247,19 @@ class SecureData(TelegramObject): """ def __init__(self, - personal_details=None, - passport=None, - internal_passport=None, - driver_license=None, - identity_card=None, - address=None, - utility_bill=None, - bank_statement=None, - rental_agreement=None, - passport_registration=None, - temporary_registration=None, - bot=None, - **kwargs): + personal_details: 'SecureValue' = None, + passport: 'SecureValue' = None, + internal_passport: 'SecureValue' = None, + driver_license: 'SecureValue' = None, + identity_card: 'SecureValue' = None, + address: 'SecureValue' = None, + utility_bill: 'SecureValue' = None, + bank_statement: 'SecureValue' = None, + rental_agreement: 'SecureValue' = None, + passport_registration: 'SecureValue' = None, + temporary_registration: 'SecureValue' = None, + bot: 'Bot' = None, + **kwargs: Any): # Optionals self.temporary_registration = temporary_registration self.passport_registration = passport_registration @@ -271,7 +276,9 @@ class SecureData(TelegramObject): self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureData']: + data = cls.parse_data(data) + if not data: return None @@ -320,14 +327,14 @@ class SecureValue(TelegramObject): """ def __init__(self, - data=None, - front_side=None, - reverse_side=None, - selfie=None, - files=None, - translation=None, - bot=None, - **kwargs): + data: 'DataCredentials' = None, + front_side: 'FileCredentials' = None, + reverse_side: 'FileCredentials' = None, + selfie: 'FileCredentials' = None, + files: List['FileCredentials'] = None, + translation: List['FileCredentials'] = None, + bot: 'Bot' = None, + **kwargs: Any): self.data = data self.front_side = front_side self.reverse_side = reverse_side @@ -338,7 +345,9 @@ class SecureValue(TelegramObject): self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureValue']: + data = cls.parse_data(data) + if not data: return None @@ -351,7 +360,7 @@ class SecureValue(TelegramObject): return cls(bot=bot, **data) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['files'] = [p.to_dict() for p in self.files] @@ -363,7 +372,7 @@ class SecureValue(TelegramObject): class _CredentialsBase(TelegramObject): """Base class for DataCredentials and FileCredentials.""" - def __init__(self, hash, secret, bot=None, **kwargs): + def __init__(self, hash: str, secret: str, bot: 'Bot' = None, **kwargs: Any): self.hash = hash self.secret = secret @@ -373,24 +382,6 @@ class _CredentialsBase(TelegramObject): self.bot = bot - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - - @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - credentials = list() - for c in data: - credentials.append(cls.de_json(c, bot=bot)) - - return credentials - class DataCredentials(_CredentialsBase): """ @@ -406,10 +397,10 @@ class DataCredentials(_CredentialsBase): secret (:obj:`str`): Secret of encrypted data """ - def __init__(self, data_hash, secret, **kwargs): + def __init__(self, data_hash: str, secret: str, **kwargs: Any): super().__init__(data_hash, secret, **kwargs) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() del data['file_hash'] @@ -432,10 +423,10 @@ class FileCredentials(_CredentialsBase): secret (:obj:`str`): Secret of encrypted file """ - def __init__(self, file_hash, secret, **kwargs): + def __init__(self, file_hash: str, secret: str, **kwargs: Any): super().__init__(file_hash, secret, **kwargs) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() del data['data_hash'] diff --git a/telegram/passport/data.py b/telegram/passport/data.py index 67146c62a..5bca503d8 100644 --- a/telegram/passport/data.py +++ b/telegram/passport/data.py @@ -17,6 +17,9 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. from telegram import TelegramObject +from typing import Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class PersonalDetails(TelegramObject): @@ -40,10 +43,19 @@ class PersonalDetails(TelegramObject): residence. """ - def __init__(self, first_name, last_name, birth_date, gender, country_code, - residence_country_code, first_name_native=None, - last_name_native=None, middle_name=None, - middle_name_native=None, bot=None, **kwargs): + def __init__(self, + first_name: str, + last_name: str, + birth_date: str, + gender: str, + country_code: str, + residence_country_code: str, + first_name_native: str = None, + last_name_native: str = None, + middle_name: str = None, + middle_name_native: str = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.first_name = first_name self.last_name = last_name @@ -58,13 +70,6 @@ class PersonalDetails(TelegramObject): self.bot = bot - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - class ResidentialAddress(TelegramObject): """ @@ -79,8 +84,15 @@ class ResidentialAddress(TelegramObject): post_code (:obj:`str`): Address post code. """ - def __init__(self, street_line1, street_line2, city, state, country_code, - post_code, bot=None, **kwargs): + def __init__(self, + street_line1: str, + street_line2: str, + city: str, + state: str, + country_code: str, + post_code: str, + bot: 'Bot' = None, + **kwargs: Any): # Required self.street_line1 = street_line1 self.street_line2 = street_line2 @@ -91,13 +103,6 @@ class ResidentialAddress(TelegramObject): self.bot = bot - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) - class IdDocumentData(TelegramObject): """ @@ -108,15 +113,8 @@ class IdDocumentData(TelegramObject): expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format. """ - def __init__(self, document_no, expiry_date, bot=None, **kwargs): + def __init__(self, document_no: str, expiry_date: str, bot: 'Bot' = None, **kwargs: Any): self.document_no = document_no self.expiry_date = expiry_date self.bot = bot - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(bot=bot, **data) diff --git a/telegram/passport/encryptedpassportelement.py b/telegram/passport/encryptedpassportelement.py index 8e3da4922..6139526a0 100644 --- a/telegram/passport/encryptedpassportelement.py +++ b/telegram/passport/encryptedpassportelement.py @@ -23,6 +23,11 @@ from telegram import (IdDocumentData, PassportFile, PersonalDetails, ResidentialAddress, TelegramObject) from telegram.passport.credentials import decrypt_json +from telegram.utils.types import JSONDict +from typing import List, Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, Credentials + class EncryptedPassportElement(TelegramObject): """ @@ -106,19 +111,19 @@ class EncryptedPassportElement(TelegramObject): """ def __init__(self, - type, - data=None, - phone_number=None, - email=None, - files=None, - front_side=None, - reverse_side=None, - selfie=None, - translation=None, - hash=None, - bot=None, - credentials=None, - **kwargs): + type: str, + data: PersonalDetails = None, + phone_number: str = None, + email: str = None, + files: List[PassportFile] = None, + front_side: PassportFile = None, + reverse_side: PassportFile = None, + selfie: PassportFile = None, + translation: List[PassportFile] = None, + hash: str = None, + bot: 'Bot' = None, + credentials: 'Credentials' = None, + **kwargs: Any): # Required self.type = type # Optionals @@ -138,12 +143,14 @@ class EncryptedPassportElement(TelegramObject): self.bot = bot @classmethod - def de_json(cls, data, bot): + def de_json(cls, + data: Optional[JSONDict], + bot: 'Bot') -> Optional['EncryptedPassportElement']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['files'] = PassportFile.de_list(data.get('files'), bot) or None data['front_side'] = PassportFile.de_json(data.get('front_side'), bot) data['reverse_side'] = PassportFile.de_json(data.get('reverse_side'), bot) @@ -153,12 +160,13 @@ class EncryptedPassportElement(TelegramObject): return cls(bot=bot, **data) @classmethod - def de_json_decrypted(cls, data, bot, credentials): + def de_json_decrypted(cls, + data: Optional[JSONDict], + bot: 'Bot', + credentials: 'Credentials') -> Optional['EncryptedPassportElement']: if not data: return None - data = super().de_json(data, bot) - if data['type'] not in ('phone_number', 'email'): secure_data = getattr(credentials.secure_data, data['type']) @@ -189,18 +197,7 @@ class EncryptedPassportElement(TelegramObject): return cls(bot=bot, **data) - @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - encrypted_passport_elements = list() - for element in data: - encrypted_passport_elements.append(cls.de_json(element, bot)) - - return encrypted_passport_elements - - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() if self.files: diff --git a/telegram/passport/passportdata.py b/telegram/passport/passportdata.py index e87a535bc..4159039aa 100644 --- a/telegram/passport/passportdata.py +++ b/telegram/passport/passportdata.py @@ -20,6 +20,11 @@ from telegram import EncryptedCredentials, EncryptedPassportElement, TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, List, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, Credentials + class PassportData(TelegramObject): """Contains information about Telegram Passport data shared with the bot by the user. @@ -33,7 +38,7 @@ class PassportData(TelegramObject): Args: data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information about documents and other Telegram Passport elements that was shared with the bot. - credentials (:obj:`str`): Encrypted credentials. + credentials (:class:`telegram.EncryptedCredentials`)): Encrypted credentials. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. @@ -45,27 +50,31 @@ class PassportData(TelegramObject): """ - def __init__(self, data, credentials, bot=None, **kwargs): + def __init__(self, + data: List[EncryptedPassportElement], + credentials: EncryptedCredentials, + bot: 'Bot' = None, + **kwargs: Any): self.data = data self.credentials = credentials self.bot = bot - self._decrypted_data = None + self._decrypted_data: Optional[List[EncryptedPassportElement]] = None self._id_attrs = tuple([x.type for x in data] + [credentials.hash]) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PassportData']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['data'] = EncryptedPassportElement.de_list(data.get('data'), bot) data['credentials'] = EncryptedCredentials.de_json(data.get('credentials'), bot) return cls(bot=bot, **data) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['data'] = [e.to_dict() for e in self.data] @@ -73,7 +82,7 @@ class PassportData(TelegramObject): return data @property - def decrypted_data(self): + def decrypted_data(self) -> List[EncryptedPassportElement]: """ List[:class:`telegram.EncryptedPassportElement`]: Lazily decrypt and return information about documents and other Telegram Passport elements which were shared with the bot. @@ -92,7 +101,7 @@ class PassportData(TelegramObject): return self._decrypted_data @property - def decrypted_credentials(self): + def decrypted_credentials(self) -> 'Credentials': """ :class:`telegram.Credentials`: Lazily decrypt and return credentials that were used to decrypt the data. This object also contains the user specified payload as diff --git a/telegram/passport/passportelementerrors.py b/telegram/passport/passportelementerrors.py index 95afd6a3d..d71ff6d1e 100644 --- a/telegram/passport/passportelementerrors.py +++ b/telegram/passport/passportelementerrors.py @@ -19,6 +19,7 @@ """This module contains the classes that represent Telegram PassportElementError.""" from telegram import TelegramObject +from typing import Any class PassportElementError(TelegramObject): @@ -39,7 +40,7 @@ class PassportElementError(TelegramObject): """ - def __init__(self, source, type, message, **kwargs): + def __init__(self, source: str, type: str, message: str, **kwargs: Any): # Required self.source = str(source) self.type = str(type) @@ -77,11 +78,11 @@ class PassportElementErrorDataField(PassportElementError): """ def __init__(self, - type, - field_name, - data_hash, - message, - **kwargs): + type: str, + field_name: str, + data_hash: str, + message: str, + **kwargs: Any): # Required super().__init__('data', type, message) self.field_name = field_name @@ -117,10 +118,10 @@ class PassportElementErrorFile(PassportElementError): """ def __init__(self, - type, - file_hash, - message, - **kwargs): + type: str, + file_hash: str, + message: str, + **kwargs: Any): # Required super().__init__('file', type, message) self.file_hash = file_hash @@ -155,10 +156,10 @@ class PassportElementErrorFiles(PassportElementError): """ def __init__(self, - type, - file_hashes, - message, - **kwargs): + type: str, + file_hashes: str, + message: str, + **kwargs: Any): # Required super().__init__('files', type, message) self.file_hashes = file_hashes @@ -194,10 +195,10 @@ class PassportElementErrorFrontSide(PassportElementError): """ def __init__(self, - type, - file_hash, - message, - **kwargs): + type: str, + file_hash: str, + message: str, + **kwargs: Any): # Required super().__init__('front_side', type, message) self.file_hash = file_hash @@ -232,10 +233,10 @@ class PassportElementErrorReverseSide(PassportElementError): """ def __init__(self, - type, - file_hash, - message, - **kwargs): + type: str, + file_hash: str, + message: str, + **kwargs: Any): # Required super().__init__('reverse_side', type, message) self.file_hash = file_hash @@ -268,10 +269,10 @@ class PassportElementErrorSelfie(PassportElementError): """ def __init__(self, - type, - file_hash, - message, - **kwargs): + type: str, + file_hash: str, + message: str, + **kwargs: Any): # Required super().__init__('selfie', type, message) self.file_hash = file_hash @@ -308,10 +309,10 @@ class PassportElementErrorTranslationFile(PassportElementError): """ def __init__(self, - type, - file_hash, - message, - **kwargs): + type: str, + file_hash: str, + message: str, + **kwargs: Any): # Required super().__init__('translation_file', type, message) self.file_hash = file_hash @@ -348,10 +349,10 @@ class PassportElementErrorTranslationFiles(PassportElementError): """ def __init__(self, - type, - file_hashes, - message, - **kwargs): + type: str, + file_hashes: str, + message: str, + **kwargs: Any): # Required super().__init__('translation_files', type, message) self.file_hashes = file_hashes @@ -383,10 +384,10 @@ class PassportElementErrorUnspecified(PassportElementError): """ def __init__(self, - type, - element_hash, - message, - **kwargs): + type: str, + element_hash: str, + message: str, + **kwargs: Any): # Required super().__init__('unspecified', type, message) self.element_hash = element_hash diff --git a/telegram/passport/passportfile.py b/telegram/passport/passportfile.py index 27b352496..2c892cbe6 100644 --- a/telegram/passport/passportfile.py +++ b/telegram/passport/passportfile.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Encrypted PassportFile.""" from telegram import TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, Optional, List, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot, File, FileCredentials class PassportFile(TelegramObject): @@ -52,13 +56,13 @@ class PassportFile(TelegramObject): """ def __init__(self, - file_id, - file_unique_id, - file_date, - file_size=None, - bot=None, - credentials=None, - **kwargs): + file_id: str, + file_unique_id: str, + file_date: int, + file_size: int = None, + bot: 'Bot' = None, + credentials: 'FileCredentials' = None, + **kwargs: Any): # Required self.file_id = file_id self.file_unique_id = file_unique_id @@ -71,41 +75,31 @@ class PassportFile(TelegramObject): self._id_attrs = (self.file_unique_id,) @classmethod - def de_json(cls, data, bot): + def de_json_decrypted(cls, + data: Optional[JSONDict], + bot: 'Bot', + credentials: 'FileCredentials') -> Optional['PassportFile']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - - return cls(bot=bot, **data) - - @classmethod - def de_json_decrypted(cls, data, bot, credentials): - if not data: - return None - - data = super().de_json(data, bot) - data['credentials'] = credentials return cls(bot=bot, **data) @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - return [cls.de_json(passport_file, bot) for passport_file in data] - - @classmethod - def de_list_decrypted(cls, data, bot, credentials): + def de_list_decrypted(cls, + data: Optional[List[JSONDict]], + bot: 'Bot', + credentials: List['FileCredentials']) -> List[Optional['PassportFile']]: if not data: return [] return [cls.de_json_decrypted(passport_file, bot, credentials[i]) for i, passport_file in enumerate(data)] - def get_file(self, timeout=None, api_kwargs=None): + def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File': """ Wrapper over :attr:`telegram.Bot.get_file`. Will automatically assign the correct credentials to the returned :class:`telegram.File` if originating from diff --git a/telegram/payment/invoice.py b/telegram/payment/invoice.py index 670f54cd6..f6af09150 100644 --- a/telegram/payment/invoice.py +++ b/telegram/payment/invoice.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram Invoice.""" from telegram import TelegramObject +from typing import Any class Invoice(TelegramObject): @@ -51,7 +52,13 @@ class Invoice(TelegramObject): """ - def __init__(self, title, description, start_parameter, currency, total_amount, **kwargs): + def __init__(self, + title: str, + description: str, + start_parameter: str, + currency: str, + total_amount: int, + **kwargs: Any): self.title = title self.description = description self.start_parameter = start_parameter @@ -65,10 +72,3 @@ class Invoice(TelegramObject): self.currency, self.total_amount, ) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegram/payment/labeledprice.py b/telegram/payment/labeledprice.py index 71968da58..ed5db0e23 100644 --- a/telegram/payment/labeledprice.py +++ b/telegram/payment/labeledprice.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram LabeledPrice.""" from telegram import TelegramObject +from typing import Any class LabeledPrice(TelegramObject): @@ -43,7 +44,7 @@ class LabeledPrice(TelegramObject): """ - def __init__(self, label, amount, **kwargs): + def __init__(self, label: str, amount: int, **kwargs: Any): self.label = label self.amount = amount diff --git a/telegram/payment/orderinfo.py b/telegram/payment/orderinfo.py index bd5d66110..9709acbc6 100644 --- a/telegram/payment/orderinfo.py +++ b/telegram/payment/orderinfo.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram OrderInfo.""" from telegram import TelegramObject, ShippingAddress +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class OrderInfo(TelegramObject): @@ -43,7 +47,12 @@ class OrderInfo(TelegramObject): """ - def __init__(self, name=None, phone_number=None, email=None, shipping_address=None, **kwargs): + def __init__(self, + name: str = None, + phone_number: str = None, + email: str = None, + shipping_address: str = None, + **kwargs: Any): self.name = name self.phone_number = phone_number self.email = email @@ -52,12 +61,12 @@ class OrderInfo(TelegramObject): self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['OrderInfo']: + data = cls.parse_data(data) + if not data: return cls() - data = super().de_json(data, bot) - data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot) return cls(**data) diff --git a/telegram/payment/precheckoutquery.py b/telegram/payment/precheckoutquery.py index 2e82cb49f..eb1c1f372 100644 --- a/telegram/payment/precheckoutquery.py +++ b/telegram/payment/precheckoutquery.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram PreCheckoutQuery.""" from telegram import TelegramObject, User, OrderInfo +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class PreCheckoutQuery(TelegramObject): @@ -61,15 +65,15 @@ class PreCheckoutQuery(TelegramObject): """ def __init__(self, - id, - from_user, - currency, - total_amount, - invoice_payload, - shipping_option_id=None, - order_info=None, - bot=None, - **kwargs): + id: str, + from_user: User, + currency: str, + total_amount: int, + invoice_payload: str, + shipping_option_id: str = None, + order_info: OrderInfo = None, + bot: 'Bot' = None, + **kwargs: Any): self.id = id self.from_user = from_user self.currency = currency @@ -83,18 +87,18 @@ class PreCheckoutQuery(TelegramObject): self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PreCheckoutQuery']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['from_user'] = User.de_json(data.pop('from'), bot) data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot) return cls(bot=bot, **data) - def answer(self, *args, **kwargs): + def answer(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.answer_pre_checkout_query(update.pre_checkout_query.id, *args, **kwargs) diff --git a/telegram/payment/shippingaddress.py b/telegram/payment/shippingaddress.py index a51b4d1cc..91876a139 100644 --- a/telegram/payment/shippingaddress.py +++ b/telegram/payment/shippingaddress.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram ShippingAddress.""" from telegram import TelegramObject +from typing import Any class ShippingAddress(TelegramObject): @@ -47,7 +48,14 @@ class ShippingAddress(TelegramObject): """ - def __init__(self, country_code, state, city, street_line1, street_line2, post_code, **kwargs): + def __init__(self, + country_code: str, + state: str, + city: str, + street_line1: str, + street_line2: str, + post_code: str, + **kwargs: Any): self.country_code = country_code self.state = state self.city = city @@ -57,10 +65,3 @@ class ShippingAddress(TelegramObject): self._id_attrs = (self.country_code, self.state, self.city, self.street_line1, self.street_line2, self.post_code) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/telegram/payment/shippingoption.py b/telegram/payment/shippingoption.py index 4a05b3758..f08a8ab95 100644 --- a/telegram/payment/shippingoption.py +++ b/telegram/payment/shippingoption.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram ShippingOption.""" from telegram import TelegramObject +from telegram.utils.types import JSONDict +from typing import List, Any, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import LabeledPrice # noqa class ShippingOption(TelegramObject): @@ -40,14 +44,14 @@ class ShippingOption(TelegramObject): """ - def __init__(self, id, title, prices, **kwargs): + def __init__(self, id: str, title: str, prices: List['LabeledPrice'], **kwargs: Any): self.id = id self.title = title self.prices = prices self._id_attrs = (self.id,) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['prices'] = [p.to_dict() for p in self.prices] diff --git a/telegram/payment/shippingquery.py b/telegram/payment/shippingquery.py index 3b2e1c33a..9a5bb70df 100644 --- a/telegram/payment/shippingquery.py +++ b/telegram/payment/shippingquery.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram ShippingQuery.""" from telegram import TelegramObject, User, ShippingAddress +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class ShippingQuery(TelegramObject): @@ -47,7 +51,13 @@ class ShippingQuery(TelegramObject): """ - def __init__(self, id, from_user, invoice_payload, shipping_address, bot=None, **kwargs): + def __init__(self, + id: str, + from_user: User, + invoice_payload: str, + shipping_address: ShippingAddress, + bot: 'Bot' = None, + **kwargs: Any): self.id = id self.from_user = from_user self.invoice_payload = invoice_payload @@ -58,18 +68,18 @@ class ShippingQuery(TelegramObject): self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ShippingQuery']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['from_user'] = User.de_json(data.pop('from'), bot) data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot) return cls(bot=bot, **data) - def answer(self, *args, **kwargs): + def answer(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.answer_shipping_query(update.shipping_query.id, *args, **kwargs) diff --git a/telegram/payment/successfulpayment.py b/telegram/payment/successfulpayment.py index 0d08e66ab..a388cb11b 100644 --- a/telegram/payment/successfulpayment.py +++ b/telegram/payment/successfulpayment.py @@ -19,6 +19,10 @@ """This module contains an object that represents a Telegram SuccessfulPayment.""" from telegram import TelegramObject, OrderInfo +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import Bot class SuccessfulPayment(TelegramObject): @@ -57,14 +61,14 @@ class SuccessfulPayment(TelegramObject): """ def __init__(self, - currency, - total_amount, - invoice_payload, - telegram_payment_charge_id, - provider_payment_charge_id, - shipping_option_id=None, - order_info=None, - **kwargs): + currency: str, + total_amount: int, + invoice_payload: str, + telegram_payment_charge_id: str, + provider_payment_charge_id: str, + shipping_option_id: str = None, + order_info: OrderInfo = None, + **kwargs: Any): self.currency = currency self.total_amount = total_amount self.invoice_payload = invoice_payload @@ -76,11 +80,12 @@ class SuccessfulPayment(TelegramObject): self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SuccessfulPayment']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot) return cls(**data) diff --git a/telegram/poll.py b/telegram/poll.py index d49dd0266..a95b16147 100644 --- a/telegram/poll.py +++ b/telegram/poll.py @@ -20,9 +20,15 @@ """This module contains an object that represents a Telegram Poll.""" import sys +import datetime from telegram import (TelegramObject, User, MessageEntity) from telegram.utils.helpers import to_timestamp, from_timestamp +from telegram.utils.types import JSONDict +from typing import Any, Dict, Optional, List, TYPE_CHECKING + +if TYPE_CHECKING: + from telegram import Bot class PollOption(TelegramObject): @@ -42,19 +48,12 @@ class PollOption(TelegramObject): """ - def __init__(self, text, voter_count, **kwargs): + def __init__(self, text: str, voter_count: int, **kwargs: Any): self.text = text self.voter_count = voter_count self._id_attrs = (self.text, self.voter_count) - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) - class PollAnswer(TelegramObject): """ @@ -75,7 +74,7 @@ class PollAnswer(TelegramObject): May be empty if the user retracted their vote. """ - def __init__(self, poll_id, user, option_ids, **kwargs): + def __init__(self, poll_id: str, user: User, option_ids: List[int], **kwargs: Any): self.poll_id = poll_id self.user = user self.option_ids = option_ids @@ -83,12 +82,12 @@ class PollAnswer(TelegramObject): self._id_attrs = (self.poll_id, self.user, tuple(self.option_ids)) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['PollAnswer']: + data = cls.parse_data(data) + if not data: return None - data = super(PollAnswer, cls).de_json(data, bot) - data['user'] = User.de_json(data.get('user'), bot) return cls(**data) @@ -143,20 +142,20 @@ class Poll(TelegramObject): """ def __init__(self, - id, - question, - options, - total_voter_count, - is_closed, - is_anonymous, - type, - allows_multiple_answers, - correct_option_id=None, - explanation=None, - explanation_entities=None, - open_period=None, - close_date=None, - **kwargs): + id: str, + question: str, + options: List[PollOption], + total_voter_count: int, + is_closed: bool, + is_anonymous: bool, + type: str, + allows_multiple_answers: bool, + correct_option_id: int = None, + explanation: str = None, + explanation_entities: List[MessageEntity] = None, + open_period: int = None, + close_date: datetime.datetime = None, + **kwargs: Any): self.id = id self.question = question self.options = options @@ -174,19 +173,19 @@ class Poll(TelegramObject): self._id_attrs = (self.id,) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Poll']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['options'] = [PollOption.de_json(option, bot) for option in data['options']] data['explanation_entities'] = MessageEntity.de_list(data.get('explanation_entities'), bot) data['close_date'] = from_timestamp(data.get('close_date')) return cls(**data) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['options'] = [x.to_dict() for x in self.options] @@ -196,7 +195,7 @@ class Poll(TelegramObject): return data - def parse_explanation_entity(self, entity): + def parse_explanation_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. Note: @@ -211,7 +210,13 @@ class Poll(TelegramObject): Returns: :obj:`str`: The text of the given entity. + Raises: + RuntimeError: If the poll has no explanation. + """ + if not self.explanation: + raise RuntimeError("This Poll has no 'explanation'.") + # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xffff: return self.explanation[entity.offset:entity.offset + entity.length] @@ -221,7 +226,7 @@ class Poll(TelegramObject): return entity_text.decode('utf-16-le') - def parse_explanation_entities(self, types=None): + def parse_explanation_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this polls explanation filtered by their ``type`` attribute as @@ -247,10 +252,10 @@ class Poll(TelegramObject): return { entity: self.parse_explanation_entity(entity) - for entity in self.explanation_entities if entity.type in types + for entity in (self.explanation_entities or []) if entity.type in types } - REGULAR = "regular" + REGULAR: str = "regular" """:obj:`str`: 'regular'""" - QUIZ = "quiz" + QUIZ: str = "quiz" """:obj:`str`: 'quiz'""" diff --git a/telegram/replykeyboardmarkup.py b/telegram/replykeyboardmarkup.py index 35fcf8068..b0fd5897c 100644 --- a/telegram/replykeyboardmarkup.py +++ b/telegram/replykeyboardmarkup.py @@ -18,8 +18,9 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ReplyKeyboardMarkup.""" -from telegram import ReplyMarkup -from .keyboardbutton import KeyboardButton +from telegram import ReplyMarkup, KeyboardButton +from telegram.utils.types import JSONDict +from typing import List, Union, Any class ReplyKeyboardMarkup(ReplyMarkup): @@ -64,17 +65,17 @@ class ReplyKeyboardMarkup(ReplyMarkup): """ def __init__(self, - keyboard, - resize_keyboard=False, - one_time_keyboard=False, - selective=False, - **kwargs): + keyboard: List[List[Union[str, KeyboardButton]]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + **kwargs: Any): # Required self.keyboard = [] for row in keyboard: r = [] for button in row: - if hasattr(button, 'to_dict'): + if isinstance(button, KeyboardButton): r.append(button) # telegram.KeyboardButton else: r.append(KeyboardButton(button)) # str @@ -85,14 +86,14 @@ class ReplyKeyboardMarkup(ReplyMarkup): self.one_time_keyboard = bool(one_time_keyboard) self.selective = bool(selective) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['keyboard'] = [] for row in self.keyboard: - r = [] + r: List[Union[JSONDict, str]] = [] for button in row: - if hasattr(button, 'to_dict'): + if isinstance(button, KeyboardButton): r.append(button.to_dict()) # telegram.KeyboardButton else: r.append(button) # str @@ -101,11 +102,11 @@ class ReplyKeyboardMarkup(ReplyMarkup): @classmethod def from_button(cls, - button, - resize_keyboard=False, - one_time_keyboard=False, - selective=False, - **kwargs): + button: Union[KeyboardButton, str], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + **kwargs: Any) -> 'ReplyKeyboardMarkup': """Shortcut for:: ReplyKeyboardMarkup([[button]], **kwargs) @@ -142,11 +143,11 @@ class ReplyKeyboardMarkup(ReplyMarkup): @classmethod def from_row(cls, - button_row, - resize_keyboard=False, - one_time_keyboard=False, - selective=False, - **kwargs): + button_row: List[Union[str, KeyboardButton]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + **kwargs: Any) -> 'ReplyKeyboardMarkup': """Shortcut for:: ReplyKeyboardMarkup([button_row], **kwargs) @@ -184,11 +185,11 @@ class ReplyKeyboardMarkup(ReplyMarkup): @classmethod def from_column(cls, - button_column, - resize_keyboard=False, - one_time_keyboard=False, - selective=False, - **kwargs): + button_column: List[Union[str, KeyboardButton]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + **kwargs: Any) -> 'ReplyKeyboardMarkup': """Shortcut for:: ReplyKeyboardMarkup([[button] for button in button_column], **kwargs) @@ -225,7 +226,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): selective=selective, **kwargs) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): if len(self.keyboard) != len(other.keyboard): return False @@ -238,7 +239,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): return True return super(ReplyKeyboardMarkup, self).__eq__(other) # pylint: disable=no-member - def __hash__(self): + def __hash__(self) -> int: return hash(( tuple(tuple(button for button in row) for row in self.keyboard), self.resize_keyboard, self.one_time_keyboard, self.selective diff --git a/telegram/replykeyboardremove.py b/telegram/replykeyboardremove.py index edcc30835..5003eaa73 100644 --- a/telegram/replykeyboardremove.py +++ b/telegram/replykeyboardremove.py @@ -18,6 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ReplyKeyboardRemove.""" from telegram import ReplyMarkup +from typing import Any class ReplyKeyboardRemove(ReplyMarkup): @@ -53,7 +54,7 @@ class ReplyKeyboardRemove(ReplyMarkup): """ - def __init__(self, selective=False, **kwargs): + def __init__(self, selective: bool = False, **kwargs: Any): # Required self.remove_keyboard = True # Optionals diff --git a/telegram/update.py b/telegram/update.py index 5e1fa10bd..99c406654 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -21,6 +21,11 @@ from telegram import (Message, TelegramObject, InlineQuery, ChosenInlineResult, CallbackQuery, ShippingQuery, PreCheckoutQuery, Poll) from telegram.poll import PollAnswer +from telegram.utils.types import JSONDict +from typing import Any, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from telegram import Bot, User, Chat # noqa class Update(TelegramObject): @@ -84,19 +89,19 @@ class Update(TelegramObject): """ def __init__(self, - update_id, - message=None, - edited_message=None, - channel_post=None, - edited_channel_post=None, - inline_query=None, - chosen_inline_result=None, - callback_query=None, - shipping_query=None, - pre_checkout_query=None, - poll=None, - poll_answer=None, - **kwargs): + update_id: int, + message: Message = None, + edited_message: Message = None, + channel_post: Message = None, + edited_channel_post: Message = None, + inline_query: InlineQuery = None, + chosen_inline_result: ChosenInlineResult = None, + callback_query: CallbackQuery = None, + shipping_query: ShippingQuery = None, + pre_checkout_query: PreCheckoutQuery = None, + poll: Poll = None, + poll_answer: PollAnswer = None, + **kwargs: Any): # Required self.update_id = int(update_id) # Optionals @@ -112,14 +117,14 @@ class Update(TelegramObject): self.poll = poll self.poll_answer = poll_answer - self._effective_user = None - self._effective_chat = None - self._effective_message = None + self._effective_user: Optional['User'] = None + self._effective_chat: Optional['Chat'] = None + self._effective_message: Optional[Message] = None self._id_attrs = (self.update_id,) @property - def effective_user(self): + def effective_user(self) -> Optional['User']: """ :class:`telegram.User`: The user that sent this update, no matter what kind of update this is. Will be :obj:`None` for :attr:`channel_post` and :attr:`poll`. @@ -158,7 +163,7 @@ class Update(TelegramObject): return user @property - def effective_chat(self): + def effective_chat(self) -> Optional['Chat']: """ :class:`telegram.Chat`: The chat that this update was sent in, no matter what kind of update this is. Will be :obj:`None` for :attr:`inline_query`, @@ -191,7 +196,7 @@ class Update(TelegramObject): return chat @property - def effective_message(self): + def effective_message(self) -> Optional[Message]: """ :class:`telegram.Message`: The message included in this update, no matter what kind of update this is. Will be :obj:`None` for :attr:`inline_query`, @@ -224,12 +229,12 @@ class Update(TelegramObject): return message @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Update']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['message'] = Message.de_json(data.get('message'), bot) data['edited_message'] = Message.de_json(data.get('edited_message'), bot) data['inline_query'] = InlineQuery.de_json(data.get('inline_query'), bot) diff --git a/telegram/user.py b/telegram/user.py index 05676f398..633d0b381 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -23,6 +23,11 @@ from telegram import TelegramObject from telegram.utils.helpers import mention_html as util_mention_html from telegram.utils.helpers import mention_markdown as util_mention_markdown +from typing import Any, Optional, TYPE_CHECKING, List + +if TYPE_CHECKING: + from telegram import Bot, UserProfilePhotos, Message + class User(TelegramObject): """This object represents a Telegram user or bot. @@ -63,17 +68,17 @@ class User(TelegramObject): """ def __init__(self, - id, - first_name, - is_bot, - last_name=None, - username=None, - language_code=None, - can_join_groups=None, - can_read_all_group_messages=None, - supports_inline_queries=None, - bot=None, - **kwargs): + id: int, + first_name: str, + is_bot: bool, + last_name: str = None, + username: str = None, + language_code: str = None, + can_join_groups: bool = None, + can_read_all_group_messages: bool = None, + supports_inline_queries: bool = None, + bot: 'Bot' = None, + **kwargs: Any): # Required self.id = int(id) self.first_name = first_name @@ -90,7 +95,7 @@ class User(TelegramObject): self._id_attrs = (self.id,) @property - def name(self): + def name(self) -> str: """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`.""" if self.username: @@ -98,7 +103,7 @@ class User(TelegramObject): return self.full_name @property - def full_name(self): + def full_name(self) -> str: """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if available) :attr:`last_name`.""" @@ -107,7 +112,7 @@ class User(TelegramObject): return self.first_name @property - def link(self): + def link(self) -> Optional[str]: """:obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link of the user.""" @@ -115,15 +120,7 @@ class User(TelegramObject): return "https://t.me/{}".format(self.username) return None - @classmethod - def de_json(cls, data, bot): - if not data: - return None - data = super().de_json(data, bot) - - return cls(bot=bot, **data) - - def get_profile_photos(self, *args, **kwargs): + def get_profile_photos(self, *args: Any, **kwargs: Any) -> 'UserProfilePhotos': """ Shortcut for:: @@ -133,18 +130,7 @@ class User(TelegramObject): return self.bot.get_user_profile_photos(self.id, *args, **kwargs) - @classmethod - def de_list(cls, data, bot): - if not data: - return [] - - users = list() - for user in data: - users.append(cls.de_json(user, bot)) - - return users - - def mention_markdown(self, name=None): + def mention_markdown(self, name: str = None) -> str: """ Note: :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for @@ -161,7 +147,7 @@ class User(TelegramObject): return util_mention_markdown(self.id, name) return util_mention_markdown(self.id, self.full_name) - def mention_markdown_v2(self, name=None): + def mention_markdown_v2(self, name: str = None) -> str: """ Args: name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. @@ -174,7 +160,7 @@ class User(TelegramObject): return util_mention_markdown(self.id, name, version=2) return util_mention_markdown(self.id, self.full_name, version=2) - def mention_html(self, name=None): + def mention_html(self, name: str = None) -> str: """ Args: name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. @@ -187,7 +173,7 @@ class User(TelegramObject): return util_mention_html(self.id, name) return util_mention_html(self.id, self.full_name) - def send_message(self, *args, **kwargs): + def send_message(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_message(update.effective_user.id, *args, **kwargs) @@ -198,7 +184,7 @@ class User(TelegramObject): """ return self.bot.send_message(self.id, *args, **kwargs) - def send_photo(self, *args, **kwargs): + def send_photo(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_photo(update.effective_user.id, *args, **kwargs) @@ -209,7 +195,7 @@ class User(TelegramObject): """ return self.bot.send_photo(self.id, *args, **kwargs) - def send_media_group(self, *args, **kwargs): + def send_media_group(self, *args: Any, **kwargs: Any) -> List['Message']: """Shortcut for:: bot.send_media_group(update.effective_user.id, *args, **kwargs) @@ -220,7 +206,7 @@ class User(TelegramObject): """ return self.bot.send_media_group(self.id, *args, **kwargs) - def send_audio(self, *args, **kwargs): + def send_audio(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_audio(update.effective_user.id, *args, **kwargs) @@ -231,7 +217,7 @@ class User(TelegramObject): """ return self.bot.send_audio(self.id, *args, **kwargs) - def send_chat_action(self, *args, **kwargs): + def send_chat_action(self, *args: Any, **kwargs: Any) -> bool: """Shortcut for:: bot.send_chat_action(update.effective_user.id, *args, **kwargs) @@ -245,7 +231,7 @@ class User(TelegramObject): send_action = send_chat_action """Alias for :attr:`send_chat_action`""" - def send_contact(self, *args, **kwargs): + def send_contact(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_contact(update.effective_user.id, *args, **kwargs) @@ -256,7 +242,7 @@ class User(TelegramObject): """ return self.bot.send_contact(self.id, *args, **kwargs) - def send_dice(self, *args, **kwargs): + def send_dice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_dice(update.effective_user.id, *args, **kwargs) @@ -267,7 +253,7 @@ class User(TelegramObject): """ return self.bot.send_dice(self.id, *args, **kwargs) - def send_document(self, *args, **kwargs): + def send_document(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_document(update.effective_user.id, *args, **kwargs) @@ -278,7 +264,7 @@ class User(TelegramObject): """ return self.bot.send_document(self.id, *args, **kwargs) - def send_game(self, *args, **kwargs): + def send_game(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_game(update.effective_user.id, *args, **kwargs) @@ -289,7 +275,7 @@ class User(TelegramObject): """ return self.bot.send_game(self.id, *args, **kwargs) - def send_invoice(self, *args, **kwargs): + def send_invoice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_invoice(update.effective_user.id, *args, **kwargs) @@ -300,7 +286,7 @@ class User(TelegramObject): """ return self.bot.send_invoice(self.id, *args, **kwargs) - def send_location(self, *args, **kwargs): + def send_location(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_location(update.effective_user.id, *args, **kwargs) @@ -311,7 +297,7 @@ class User(TelegramObject): """ return self.bot.send_location(self.id, *args, **kwargs) - def send_animation(self, *args, **kwargs): + def send_animation(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_animation(update.effective_user.id, *args, **kwargs) @@ -322,7 +308,7 @@ class User(TelegramObject): """ return self.bot.send_animation(self.id, *args, **kwargs) - def send_sticker(self, *args, **kwargs): + def send_sticker(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_sticker(update.effective_user.id, *args, **kwargs) @@ -333,7 +319,7 @@ class User(TelegramObject): """ return self.bot.send_sticker(self.id, *args, **kwargs) - def send_video(self, *args, **kwargs): + def send_video(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_video(update.effective_user.id, *args, **kwargs) @@ -344,7 +330,7 @@ class User(TelegramObject): """ return self.bot.send_video(self.id, *args, **kwargs) - def send_venue(self, *args, **kwargs): + def send_venue(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_venue(update.effective_user.id, *args, **kwargs) @@ -355,7 +341,7 @@ class User(TelegramObject): """ return self.bot.send_venue(self.id, *args, **kwargs) - def send_video_note(self, *args, **kwargs): + def send_video_note(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_video_note(update.effective_user.id, *args, **kwargs) @@ -366,7 +352,7 @@ class User(TelegramObject): """ return self.bot.send_video_note(self.id, *args, **kwargs) - def send_voice(self, *args, **kwargs): + def send_voice(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_voice(update.effective_user.id, *args, **kwargs) @@ -377,7 +363,7 @@ class User(TelegramObject): """ return self.bot.send_voice(self.id, *args, **kwargs) - def send_poll(self, *args, **kwargs): + def send_poll(self, *args: Any, **kwargs: Any) -> 'Message': """Shortcut for:: bot.send_poll(update.effective_user.id, *args, **kwargs) diff --git a/telegram/userprofilephotos.py b/telegram/userprofilephotos.py index fc70e1f19..773634420 100644 --- a/telegram/userprofilephotos.py +++ b/telegram/userprofilephotos.py @@ -19,6 +19,11 @@ """This module contains an object that represents a Telegram UserProfilePhotos.""" from telegram import PhotoSize, TelegramObject +from telegram.utils.types import JSONDict +from typing import Any, List, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from telegram import Bot class UserProfilePhotos(TelegramObject): @@ -38,7 +43,7 @@ class UserProfilePhotos(TelegramObject): """ - def __init__(self, total_count, photos, **kwargs): + def __init__(self, total_count: int, photos: List[List[PhotoSize]], **kwargs: Any): # Required self.total_count = int(total_count) self.photos = photos @@ -46,17 +51,17 @@ class UserProfilePhotos(TelegramObject): self._id_attrs = (self.total_count, self.photos) @classmethod - def de_json(cls, data, bot): + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['UserProfilePhotos']: + data = cls.parse_data(data) + if not data: return None - data = super().de_json(data, bot) - data['photos'] = [PhotoSize.de_list(photo, bot) for photo in data['photos']] return cls(**data) - def to_dict(self): + def to_dict(self) -> JSONDict: data = super().to_dict() data['photos'] = [] @@ -65,5 +70,5 @@ class UserProfilePhotos(TelegramObject): return data - def __hash__(self): + def __hash__(self) -> int: return hash(tuple(tuple(p for p in photo) for photo in self.photos)) diff --git a/telegram/utils/deprecate.py b/telegram/utils/deprecate.py index 73338a032..91d704614 100644 --- a/telegram/utils/deprecate.py +++ b/telegram/utils/deprecate.py @@ -19,6 +19,8 @@ """This module facilitates the deprecation of functions.""" import warnings +from typing import Callable, TypeVar, Any +RT = TypeVar('RT') # We use our own DeprecationWarning since they are muted by default and "UserWarning" makes it @@ -28,17 +30,17 @@ class TelegramDeprecationWarning(Warning): pass -def warn_deprecate_obj(old, new, stacklevel=3): +def warn_deprecate_obj(old: str, new: str, stacklevel: int = 3) -> None: warnings.warn( '{} is being deprecated, please use {} from now on.'.format(old, new), category=TelegramDeprecationWarning, stacklevel=stacklevel) -def deprecate(func, old, new): +def deprecate(func: Callable[..., RT], old: str, new: str) -> Callable[..., RT]: """Warn users invoking old to switch to the new function.""" - def f(*args, **kwargs): + def f(*args: Any, **kwargs: Any) -> RT: warn_deprecate_obj(old, new) return func(*args, **kwargs) diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 19287b0f7..296ba3bfb 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -31,21 +31,26 @@ import pytz try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] +from telegram.utils.types import JSONDict +from typing import Union, Any, Optional, Dict, DefaultDict, Tuple, TYPE_CHECKING +if TYPE_CHECKING: + from telegram import MessageEntity + # From https://stackoverflow.com/questions/2549939/get-signal-names-from-numbers-in-python _signames = {v: k for k, v in reversed(sorted(vars(signal).items())) if k.startswith('SIG') and not k.startswith('SIG_')} -def get_signal_name(signum): +def get_signal_name(signum: int) -> str: """Returns the signal name of the given signal number.""" return _signames[signum] -def escape_markdown(text, version=1, entity_type=None): +def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: """ Helper function to escape telegram markup symbols. @@ -74,18 +79,18 @@ def escape_markdown(text, version=1, entity_type=None): # -------- date/time related helpers -------- -def _datetime_to_float_timestamp(dt_obj): +def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float: """ Converts a datetime object to a float timestamp (with sub-second precision). If the datetime object is timezone-naive, it is assumed to be in UTC. """ - if dt_obj.tzinfo is None: dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc) return dt_obj.timestamp() -def to_float_timestamp(t, reference_timestamp=None, tzinfo=None): +def to_float_timestamp(t: Union[int, float, dtm.timedelta, dtm.datetime, dtm.time], + reference_timestamp: float = None, tzinfo: pytz.BaseTzInfo = None) -> float: """ Converts a given time object to a float POSIX timestamp. Used to convert different time specifications to a common format. The time object @@ -93,8 +98,6 @@ def to_float_timestamp(t, reference_timestamp=None, tzinfo=None): Any objects from the :class:`datetime` module that are timezone-naive will be assumed to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`. - :obj:`None` s are left alone (i.e. ``to_float_timestamp(None)`` is :obj:`None`). - Args: t (int | float | datetime.timedelta | datetime.datetime | datetime.time): Time value to convert. The semantics of this parameter will depend on its type: @@ -139,7 +142,7 @@ def to_float_timestamp(t, reference_timestamp=None, tzinfo=None): if isinstance(t, dtm.timedelta): return reference_timestamp + t.total_seconds() - elif isinstance(t, Number): + elif isinstance(t, (int, float)): return reference_timestamp + t if tzinfo is None: @@ -162,11 +165,15 @@ def to_float_timestamp(t, reference_timestamp=None, tzinfo=None): if t.tzinfo is None: t = tzinfo.localize(t) return _datetime_to_float_timestamp(t) + elif isinstance(t, Number): + return reference_timestamp + t raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__)) -def to_timestamp(dt_obj, reference_timestamp=None, tzinfo=pytz.utc): +def to_timestamp(dt_obj: Union[int, float, dtm.timedelta, dtm.datetime, dtm.time, None], + reference_timestamp: float = None, + tzinfo: pytz.BaseTzInfo = None) -> Optional[int]: """ Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated down to the nearest integer). @@ -177,7 +184,8 @@ def to_timestamp(dt_obj, reference_timestamp=None, tzinfo=pytz.utc): if dt_obj is not None else None) -def from_timestamp(unixtime, tzinfo=pytz.utc): +def from_timestamp(unixtime: Optional[int], + tzinfo: dtm.tzinfo = pytz.utc) -> Optional[dtm.datetime]: """ Converts an (integer) unix timestamp to a timezone aware datetime object. :obj:`None`s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`). @@ -202,7 +210,7 @@ def from_timestamp(unixtime, tzinfo=pytz.utc): # -------- end -------- -def mention_html(user_id, name): +def mention_html(user_id: int, name: str) -> Optional[str]: """ Args: user_id (:obj:`int`) The user's id which you want to mention. @@ -215,7 +223,7 @@ def mention_html(user_id, name): return u'{}'.format(user_id, escape(name)) -def mention_markdown(user_id, name, version=1): +def mention_markdown(user_id: int, name: str, version: int = 1) -> Optional[str]: """ Args: user_id (:obj:`int`) The user's id which you want to mention. @@ -230,7 +238,7 @@ def mention_markdown(user_id, name, version=1): return u'[{}](tg://user?id={})'.format(escape_markdown(name, version=version), user_id) -def effective_message_type(entity): +def effective_message_type(entity: 'MessageEntity') -> Optional[str]: """ Extracts the type of message as a string identifier from a :class:`telegram.Message` or a :class:`telegram.Update`. @@ -261,7 +269,7 @@ def effective_message_type(entity): return None -def create_deep_linked_url(bot_username, payload=None, group=False): +def create_deep_linked_url(bot_username: str, payload: str = None, group: bool = False) -> str: """ Creates a deep-linked URL for this ``bot_username`` with the specified ``payload``. See https://core.telegram.org/bots#deep-linking to learn more. @@ -311,17 +319,17 @@ def create_deep_linked_url(bot_username, payload=None, group=False): ) -def encode_conversations_to_json(conversations): +def encode_conversations_to_json(conversations: Dict[str, Dict[Tuple, Any]]) -> str: """Helper method to encode a conversations dict (that uses tuples as keys) to a JSON-serializable way. Use :attr:`_decode_conversations_from_json` to decode. Args: - conversations (:obj:`dict`): The conversations dict to transofrm to JSON. + conversations (:obj:`dict`): The conversations dict to transform to JSON. Returns: :obj:`str`: The JSON-serialized conversations dict """ - tmp = {} + tmp: Dict[str, JSONDict] = {} for handler, states in conversations.items(): tmp[handler] = {} for key, state in states.items(): @@ -329,7 +337,7 @@ def encode_conversations_to_json(conversations): return json.dumps(tmp) -def decode_conversations_from_json(json_string): +def decode_conversations_from_json(json_string: str) -> Dict[str, Dict[Tuple, Any]]: """Helper method to decode a conversations dict (that uses tuples as keys) from a JSON-string created with :attr:`_encode_conversations_to_json`. @@ -340,7 +348,7 @@ def decode_conversations_from_json(json_string): :obj:`dict`: The conversations dict after decoding """ tmp = json.loads(json_string) - conversations = {} + conversations: Dict[str, Dict[Tuple, Any]] = {} for handler, states in tmp.items(): conversations[handler] = {} for key, state in states.items(): @@ -348,7 +356,7 @@ def decode_conversations_from_json(json_string): return conversations -def decode_user_chat_data_from_json(data): +def decode_user_chat_data_from_json(data: str) -> DefaultDict[int, Dict[Any, Any]]: """Helper method to decode chat or user data (that uses ints as keys) from a JSON-string. @@ -359,12 +367,12 @@ def decode_user_chat_data_from_json(data): :obj:`dict`: The user/chat_data defaultdict after decoding """ - tmp = defaultdict(dict) + tmp: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict) decoded_data = json.loads(data) - for user, data in decoded_data.items(): + for user, user_data in decoded_data.items(): user = int(user) tmp[user] = {} - for key, value in data.items(): + for key, value in user_data.items(): try: key = int(key) except ValueError: @@ -416,12 +424,12 @@ class DefaultValue: Args: value (:obj:`obj`): The value of the default argument """ - def __init__(self, value=None): + def __init__(self, value: Any = None): self.value = value - def __bool__(self): + def __bool__(self) -> bool: return bool(self.value) -DEFAULT_NONE = DefaultValue(None) +DEFAULT_NONE: DefaultValue = DefaultValue(None) """:class:`DefaultValue`: Default `None`""" diff --git a/telegram/utils/promise.py b/telegram/utils/promise.py index 08eb3bac7..49ebb5f18 100644 --- a/telegram/utils/promise.py +++ b/telegram/utils/promise.py @@ -20,6 +20,9 @@ import logging from threading import Event +from telegram.utils.types import JSONDict, HandlerArg +from typing import Callable, List, Tuple, Optional, Union, TypeVar +RT = TypeVar('RT') logger = logging.getLogger(__name__) @@ -48,17 +51,22 @@ class Promise: """ # TODO: Remove error_handling parameter once we drop the @run_async decorator - def __init__(self, pooled_function, args, kwargs, update=None, error_handling=True): + def __init__(self, + pooled_function: Callable[..., RT], + args: Union[List, Tuple], + kwargs: JSONDict, + update: HandlerArg = None, + error_handling: bool = True): self.pooled_function = pooled_function self.args = args self.kwargs = kwargs self.update = update self.error_handling = error_handling self.done = Event() - self._result = None - self._exception = None + self._result: Optional[RT] = None + self._exception: Optional[Exception] = None - def run(self): + def run(self) -> None: """Calls the :attr:`pooled_function` callable.""" try: @@ -70,10 +78,10 @@ class Promise: finally: self.done.set() - def __call__(self): + def __call__(self) -> None: self.run() - def result(self, timeout=None): + def result(self, timeout: float = None) -> Optional[RT]: """Return the result of the ``Promise``. Args: @@ -93,7 +101,7 @@ class Promise: return self._result @property - def exception(self): + def exception(self) -> Optional[Exception]: """The exception raised by :attr:`pooled_function` or ``None`` if no exception has been raised (yet).""" return self._exception diff --git a/telegram/utils/request.py b/telegram/utils/request.py index bab32b821..d7d8fcca3 100644 --- a/telegram/utils/request.py +++ b/telegram/utils/request.py @@ -26,7 +26,7 @@ import warnings try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] import certifi @@ -38,11 +38,11 @@ try: from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField except ImportError: # pragma: no cover try: - import urllib3 - import urllib3.contrib.appengine as appengine - from urllib3.connection import HTTPConnection - from urllib3.util.timeout import Timeout - from urllib3.fields import RequestField + import urllib3 # type: ignore[no-redef] + import urllib3.contrib.appengine as appengine # type: ignore[no-redef] + from urllib3.connection import HTTPConnection # type: ignore[no-redef] + from urllib3.util.timeout import Timeout # type: ignore[no-redef] + from urllib3.fields import RequestField # type: ignore[no-redef] warnings.warn('python-telegram-bot is using upstream urllib3. This is allowed but not ' 'supported by python-telegram-bot maintainers.') except ImportError: @@ -56,8 +56,11 @@ from telegram import (InputFile, TelegramError, InputMedia) from telegram.error import (Unauthorized, NetworkError, TimedOut, BadRequest, ChatMigrated, RetryAfter, InvalidToken, Conflict) +from telegram.utils.types import JSONDict +from typing import Any, Union -def _render_part(self, name, value): + +def _render_part(self: RequestField, name: str, value: str) -> str: """ Monkey patch urllib3.urllib3.fields.RequestField to make it *not* support RFC2231 compliant Content-Disposition headers since telegram servers don't understand it. Instead just escape @@ -68,7 +71,7 @@ def _render_part(self, name, value): return u'{}="{}"'.format(name, value) -RequestField._render_part = _render_part +RequestField._render_part = _render_part # type: ignore logging.getLogger('urllib3').setLevel(logging.WARNING) @@ -96,11 +99,11 @@ class Request: """ def __init__(self, - con_pool_size=1, - proxy_url=None, - urllib3_proxy_kwargs=None, - connect_timeout=5., - read_timeout=5.): + con_pool_size: int = 1, + proxy_url: str = None, + urllib3_proxy_kwargs: JSONDict = None, + connect_timeout: float = 5., + read_timeout: float = 5.): if urllib3_proxy_kwargs is None: urllib3_proxy_kwargs = dict() @@ -137,12 +140,15 @@ class Request: if not proxy_url: proxy_url = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy') + self._con_pool: Union[urllib3.PoolManager, appengine.AppEngineManager, + 'SOCKSProxyManager', # noqa: F821 + urllib3.ProxyManager] = None # type: ignore if not proxy_url: if appengine.is_appengine_sandbox(): # Use URLFetch service if running in App Engine - mgr = appengine.AppEngineManager() + self._con_pool = appengine.AppEngineManager() else: - mgr = urllib3.PoolManager(**kwargs) + self._con_pool = urllib3.PoolManager(**kwargs) else: kwargs.update(urllib3_proxy_kwargs) if proxy_url.startswith('socks'): @@ -150,7 +156,7 @@ class Request: from telegram.vendor.ptb_urllib3.urllib3.contrib.socks import SOCKSProxyManager except ImportError: raise RuntimeError('PySocks is missing') - mgr = SOCKSProxyManager(proxy_url, **kwargs) + self._con_pool = SOCKSProxyManager(proxy_url, **kwargs) else: mgr = urllib3.proxy_from_url(proxy_url, **kwargs) if mgr.proxy.auth: @@ -158,18 +164,18 @@ class Request: auth_hdrs = urllib3.make_headers(proxy_basic_auth=mgr.proxy.auth) mgr.proxy_headers.update(auth_hdrs) - self._con_pool = mgr + self._con_pool = mgr @property - def con_pool_size(self): + def con_pool_size(self) -> int: """The size of the connection pool used.""" return self._con_pool_size - def stop(self): - self._con_pool.clear() + def stop(self) -> None: + self._con_pool.clear() # type: ignore @staticmethod - def _parse(json_data): + def _parse(json_data: bytes) -> Union[JSONDict, bool]: """Try and parse the JSON returned from Telegram. Returns: @@ -198,7 +204,7 @@ class Request: return data['result'] - def _request_wrapper(self, *args, **kwargs): + def _request_wrapper(self, *args: Any, **kwargs: Any) -> bytes: """Wraps urllib3 request for handling known exceptions. Args: @@ -206,7 +212,7 @@ class Request: kwargs: keyword arguments, passed tp urllib3 request. Returns: - str: A non-parsed JSON text. + bytes: A non-parsed JSON text. Raises: TelegramError @@ -234,7 +240,7 @@ class Request: return resp.data try: - message = self._parse(resp.data) + message = str(self._parse(resp.data)) except ValueError: message = 'Unknown HTTPError' @@ -255,7 +261,10 @@ class Request: else: raise NetworkError('{} ({})'.format(message, resp.status)) - def post(self, url, data=None, timeout=None): + def post(self, + url: str, + data: JSONDict, + timeout: float = None) -> Union[JSONDict, bool]: """Request an URL. Args: @@ -293,8 +302,8 @@ class Request: if isinstance(val, InputMedia): # Attach and set val to attached name data[key] = val.to_json() - if isinstance(val.media, InputFile): - data[val.media.attach] = val.media.field_tuple + if isinstance(val.media, InputFile): # type: ignore + data[val.media.attach] = val.media.field_tuple # type: ignore else: # Attach and set val to attached name for all media = [] @@ -320,7 +329,7 @@ class Request: return self._parse(result) - def retrieve(self, url, timeout=None): + def retrieve(self, url: str, timeout: float = None) -> bytes: """Retrieve the contents of a file by its URL. Args: @@ -336,7 +345,7 @@ class Request: return self._request_wrapper('GET', url, **urlopen_kwargs) - def download(self, url, filename, timeout=None): + def download(self, url: str, filename: str, timeout: float = None) -> None: """Download a file by its URL. Args: @@ -344,9 +353,7 @@ class Request: timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). - - filename: - The filename within the path to download the file. + filename (:obj:`str`): The filename within the path to download the file. """ buf = self.retrieve(url, timeout=timeout) diff --git a/telegram/utils/types.py b/telegram/utils/types.py new file mode 100644 index 000000000..bbaecb1a2 --- /dev/null +++ b/telegram/utils/types.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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 custom typing aliases.""" +from typing import Union, Any, Dict, TYPE_CHECKING, IO, Tuple, Optional + +if TYPE_CHECKING: + from telegram import InputFile, Update + +FileLike = Union[IO, 'InputFile'] +"""Either an open file handler or in :class:`telegram.InputFile`.""" + +JSONDict = Dict[str, Any] +"""Dictionary containing response from Telegram or data to send to the API.""" + +HandlerArg = Union[str, 'Update'] +"""The argument that handlers parse for :meth:`telegram.ext.handler.check_update` etc.""" + +ConversationDict = Dict[Tuple[int, ...], Optional[object]] +"""Dicts as maintained by the :class:`telegram.ext.ConversationHandler`.""" diff --git a/telegram/utils/webhookhandler.py b/telegram/utils/webhookhandler.py index d5afccf01..9f012b9c1 100644 --- a/telegram/utils/webhookhandler.py +++ b/telegram/utils/webhookhandler.py @@ -21,20 +21,32 @@ import os import sys import logging from telegram import Update -from threading import Lock +from threading import Lock, Event try: import ujson as json except ImportError: - import json + import json # type: ignore[no-redef] from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop import tornado.web +from ssl import SSLContext +from queue import Queue +from telegram.utils.types import JSONDict +from typing import Any, TYPE_CHECKING +from tornado import httputil +if TYPE_CHECKING: + from telegram import Bot + class WebhookServer: - def __init__(self, listen, port, webhook_app, ssl_ctx): + def __init__(self, + listen: str, + port: int, + webhook_app: 'WebhookAppClass', + ssl_ctx: SSLContext): self.http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx) self.listen = listen self.port = port @@ -44,7 +56,7 @@ class WebhookServer: self.server_lock = Lock() self.shutdown_lock = Lock() - def serve_forever(self, force_event_loop=False, ready=None): + def serve_forever(self, force_event_loop: bool = False, ready: Event = None) -> None: with self.server_lock: self.is_running = True self.logger.debug('Webhook Server started.') @@ -55,24 +67,24 @@ class WebhookServer: if ready is not None: ready.set() - self.loop.start() + self.loop.start() # type: ignore self.logger.debug('Webhook Server stopped.') self.is_running = False - def shutdown(self): + def shutdown(self) -> None: with self.shutdown_lock: if not self.is_running: self.logger.warning('Webhook Server already stopped.') return else: - self.loop.add_callback(self.loop.stop) + self.loop.add_callback(self.loop.stop) # type: ignore - def handle_error(self, request, client_address): + def handle_error(self, request: Any, client_address: str) -> None: """Handle an error gracefully.""" self.logger.debug('Exception happened during processing of request from %s', client_address, exc_info=True) - def _ensure_event_loop(self, force_event_loop=False): + def _ensure_event_loop(self, force_event_loop: bool = False) -> None: """If there's no asyncio event loop set for the current thread - create one.""" try: loop = asyncio.get_event_loop() @@ -111,7 +123,10 @@ class WebhookServer: class WebhookAppClass(tornado.web.Application): - def __init__(self, webhook_path, bot, update_queue): + def __init__(self, + webhook_path: str, + bot: 'Bot', + update_queue: Queue): self.shared_objects = {"bot": bot, "update_queue": update_queue} handlers = [ (r"{}/?".format(webhook_path), WebhookHandler, @@ -119,7 +134,7 @@ class WebhookAppClass(tornado.web.Application): ] # noqa tornado.web.Application.__init__(self, handlers) - def log_request(self, handler): + def log_request(self, handler: tornado.web.RequestHandler) -> None: pass @@ -127,18 +142,21 @@ class WebhookAppClass(tornado.web.Application): class WebhookHandler(tornado.web.RequestHandler): SUPPORTED_METHODS = ["POST"] - def __init__(self, application, request, **kwargs): + def __init__(self, + application: tornado.web.Application, + request: httputil.HTTPServerRequest, + **kwargs: JSONDict): super().__init__(application, request, **kwargs) self.logger = logging.getLogger(__name__) - def initialize(self, bot, update_queue): + def initialize(self, bot: 'Bot', update_queue: Queue) -> None: self.bot = bot self.update_queue = update_queue - def set_default_headers(self): + def set_default_headers(self) -> None: self.set_header("Content-Type", 'application/json; charset="utf-8"') - def post(self): + def post(self) -> None: self.logger.debug('Webhook triggered') self._validate_post() json_string = self.request.body.decode() @@ -146,15 +164,16 @@ class WebhookHandler(tornado.web.RequestHandler): self.set_status(200) self.logger.debug('Webhook received data: ' + json_string) update = Update.de_json(data, self.bot) - self.logger.debug('Received Update with ID %d on Webhook' % update.update_id) - self.update_queue.put(update) + if update: + self.logger.debug('Received Update with ID %d on Webhook' % update.update_id) + self.update_queue.put(update) - def _validate_post(self): + def _validate_post(self) -> None: ct_header = self.request.headers.get("Content-Type", None) if ct_header != 'application/json': raise tornado.web.HTTPError(403) - def write_error(self, status_code, **kwargs): + def write_error(self, status_code: int, **kwargs: Any) -> None: """Log an arbitrary message. This is used by all other logging functions. diff --git a/telegram/webhookinfo.py b/telegram/webhookinfo.py index 21ccacc9c..9dfc81bb4 100644 --- a/telegram/webhookinfo.py +++ b/telegram/webhookinfo.py @@ -19,6 +19,7 @@ """This module contains an object that represents a Telegram WebhookInfo.""" from telegram import TelegramObject +from typing import Any, List class WebhookInfo(TelegramObject): @@ -59,14 +60,14 @@ class WebhookInfo(TelegramObject): """ def __init__(self, - url, - has_custom_certificate, - pending_update_count, - last_error_date=None, - last_error_message=None, - max_connections=None, - allowed_updates=None, - **kwargs): + url: str, + has_custom_certificate: bool, + pending_update_count: int, + last_error_date: int = None, + last_error_message: str = None, + max_connections: int = None, + allowed_updates: List[str] = None, + **kwargs: Any): # Required self.url = url self.has_custom_certificate = has_custom_certificate @@ -85,10 +86,3 @@ class WebhookInfo(TelegramObject): self.max_connections, self.allowed_updates, ) - - @classmethod - def de_json(cls, data, bot): - if not data: - return None - - return cls(**data) diff --git a/tests/conftest.py b/tests/conftest.py index f6d5c1569..4f6926b89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,6 +55,8 @@ def bot(bot_info): DEFAULT_BOTS = {} + + @pytest.fixture(scope='function') def default_bot(request, bot_info): param = request.param if hasattr(request, 'param') else {} @@ -269,7 +271,7 @@ def mock_filter(request): def get_false_update_fixture_decorator_params(): - message = Message(1, User(1, '', False), DATE, Chat(1, ''), text='test') + message = Message(1, DATE, Chat(1, ''), from_user=User(1, '', False), text='test') params = [ {'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)}, {'channel_post': message}, diff --git a/tests/test_bot.py b/tests/test_bot.py index aa78ead13..b6d7f6383 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -455,7 +455,7 @@ class TestBot: results = data['results'] length_matches = len(results) == num_results ids_match = all([int(res['id']) == id_offset + i for i, res in enumerate(results)]) - next_offset_matches = data['next_offset'] == expected_next_offset + next_offset_matches = data['next_offset'] == str(expected_next_offset) return length_matches and ids_match and next_offset_matches monkeypatch.setattr(bot.request, 'post', make_assertion) @@ -468,7 +468,7 @@ class TestBot: results = data['results'] length_matches = len(results) == MAX_INLINE_QUERY_RESULTS ids_match = all([int(res['id']) == 1 + i for i, res in enumerate(results)]) - next_offset_matches = data['next_offset'] == 1 + next_offset_matches = data['next_offset'] == '1' return length_matches and ids_match and next_offset_matches monkeypatch.setattr(bot.request, 'post', make_assertion) @@ -494,7 +494,7 @@ class TestBot: results = data['results'] length = len(results) == 5 ids = all([int(res['id']) == 6 + i for i, res in enumerate(results)]) - next_offset = data['next_offset'] == 2 + next_offset = data['next_offset'] == '2' return length and ids and next_offset monkeypatch.setattr(bot.request, 'post', make_assertion) diff --git a/tests/test_callbackcontext.py b/tests/test_callbackcontext.py index e982302a7..f3806ea0f 100644 --- a/tests/test_callbackcontext.py +++ b/tests/test_callbackcontext.py @@ -41,7 +41,8 @@ class TestCallbackContext: assert callback_context.update_queue is cdp.update_queue def test_from_update(self, cdp): - update = Update(0, message=Message(0, User(1, 'user', False), None, Chat(1, 'chat'))) + update = Update(0, message=Message(0, None, Chat(1, 'chat'), + from_user=User(1, 'user', False))) callback_context = CallbackContext.from_update(update, cdp) @@ -62,8 +63,8 @@ class TestCallbackContext: assert callback_context_same_user_chat.chat_data is callback_context.chat_data assert callback_context_same_user_chat.user_data is callback_context.user_data - update_other_user_chat = Update(0, message=Message(0, User(2, 'user', False), - None, Chat(2, 'chat'))) + update_other_user_chat = Update(0, message=Message(0, None, Chat(2, 'chat'), + from_user=User(2, 'user', False))) callback_context_other_user_chat = CallbackContext.from_update(update_other_user_chat, cdp) @@ -93,7 +94,8 @@ class TestCallbackContext: def test_from_error(self, cdp): error = TelegramError('test') - update = Update(0, message=Message(0, User(1, 'user', False), None, Chat(1, 'chat'))) + update = Update(0, message=Message(0, None, Chat(1, 'chat'), + from_user=User(1, 'user', False))) callback_context = CallbackContext.from_error(update, error, cdp) @@ -131,7 +133,8 @@ class TestCallbackContext: assert callback_context.match == 'test' def test_data_assignment(self, cdp): - update = Update(0, message=Message(0, User(1, 'user', False), None, Chat(1, 'chat'))) + update = Update(0, message=Message(0, None, Chat(1, 'chat'), + from_user=User(1, 'user', False))) callback_context = CallbackContext.from_update(update, cdp) diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 183269e59..be6d4afa9 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -41,7 +41,7 @@ class TestCallbackQuery: id_ = 'id' from_user = User(1, 'test_user', False) chat_instance = 'chat_instance' - message = Message(3, User(5, 'bot', False), None, Chat(4, 'private')) + message = Message(3, None, Chat(4, 'private'), from_user=User(5, 'bot', False)) data = 'data' inline_message_id = 'inline_message_id' game_short_name = 'the_game' @@ -82,7 +82,7 @@ class TestCallbackQuery: def test(*args, **kwargs): return args[0] == callback_query.id - monkeypatch.setattr(callback_query.bot, 'answerCallbackQuery', test) + monkeypatch.setattr(callback_query.bot, 'answer_callback_query', test) # TODO: PEP8 assert callback_query.answer() diff --git a/tests/test_callbackqueryhandler.py b/tests/test_callbackqueryhandler.py index d81ef59f5..d3960eb4b 100644 --- a/tests/test_callbackqueryhandler.py +++ b/tests/test_callbackqueryhandler.py @@ -24,7 +24,7 @@ from telegram import (Update, CallbackQuery, Bot, Message, User, Chat, InlineQue ChosenInlineResult, ShippingQuery, PreCheckoutQuery) from telegram.ext import CallbackQueryHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_choseninlineresulthandler.py b/tests/test_choseninlineresulthandler.py index f09479e8b..5a3d4d1dd 100644 --- a/tests/test_choseninlineresulthandler.py +++ b/tests/test_choseninlineresulthandler.py @@ -24,7 +24,7 @@ from telegram import (Update, Chat, Bot, ChosenInlineResult, User, Message, Call InlineQuery, ShippingQuery, PreCheckoutQuery) from telegram.ext import ChosenInlineResultHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index d6dafce79..e0f8650c8 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -270,7 +270,7 @@ class TestConversationHandler: dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -307,7 +307,7 @@ class TestConversationHandler: fallbacks=self.fallbacks) dp.add_handler(handler) - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -334,7 +334,7 @@ class TestConversationHandler: dp.add_handler(handler) # first check if fallback will not trigger start when not started - message = Message(0, user1, None, self.group, text='/eat', + message = Message(0, None, self.group, from_user=user1, text='/eat', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/eat'))], bot=bot) @@ -369,7 +369,7 @@ class TestConversationHandler: dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -398,7 +398,7 @@ class TestConversationHandler: dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -437,7 +437,8 @@ class TestConversationHandler: dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='msg w/ inlinekeyboard', bot=bot) + message = Message(0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', + bot=bot) cbq = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) dp.process_update(Update(update_id=0, callback_query=cbq)) @@ -462,7 +463,7 @@ class TestConversationHandler: dp.add_handler(handler) # User starts the state machine and immediately ends it. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -479,7 +480,7 @@ class TestConversationHandler: # User starts the state machine with an async function that immediately ends the # conversation. Async results are resolved when the users state is queried next time. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -503,7 +504,7 @@ class TestConversationHandler: # User starts the state machine with an async function that immediately ends the # conversation. Async results are resolved when the users state is queried next time. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, text='/start', from_user=user1, entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -525,7 +526,7 @@ class TestConversationHandler: dp.add_handler(handler) # User starts the state machine and a callback function returns None - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, None, self.group, from_user=user1, text='/start', bot=bot) dp.process_update(Update(update_id=0, message=message)) assert len(handler.conversations) == 0 @@ -538,7 +539,7 @@ class TestConversationHandler: # User starts the state machine with an async function that returns None # Async results are resolved when the users state is queried next time. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -561,7 +562,7 @@ class TestConversationHandler: # User starts the state machine with an async function that returns None # Async results are resolved when the users state is queried next time. - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, text='/start', from_user=user1, entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -594,7 +595,7 @@ class TestConversationHandler: def test_all_update_types(self, dp, bot, user1): handler = ConversationHandler(entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[]) - message = Message(0, user1, None, self.group, text='ignore', bot=bot) + message = Message(0, None, self.group, from_user=user1, text='ignore', bot=bot) callback_query = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) chosen_inline_result = ChosenInlineResult(0, user1, 'query', bot=bot) inline_query = InlineQuery(0, user1, 'query', 0, bot=bot) @@ -613,7 +614,7 @@ class TestConversationHandler: dp.add_handler(handler) # Start state machine, then reach timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -643,7 +644,7 @@ class TestConversationHandler: dp.add_handler(handler) # Start state machine, then reach timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, text='/start', from_user=user1, entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -674,7 +675,7 @@ class TestConversationHandler: cdp.add_handler(handler) # Start state machine, then reach timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -705,7 +706,7 @@ class TestConversationHandler: # t=.6 /pourCoffee (timeout=1.1) # t=.75 second timeout # t=1.1 actual timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -734,7 +735,7 @@ class TestConversationHandler: dp.add_handler(handler) # Start state machine, do something as second user, then reach timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -765,7 +766,7 @@ class TestConversationHandler: dp.add_handler(handler) # CommandHandler timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -810,7 +811,7 @@ class TestConversationHandler: cdp.add_handler(handler) # CommandHandler timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -871,7 +872,7 @@ class TestConversationHandler: dp.add_handler(handler) # CommandHandler timeout - message = Message(0, user1, None, self.group, text='/start', + message = Message(0, None, self.group, from_user=user1, text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))], bot=bot) @@ -954,7 +955,7 @@ class TestConversationHandler: dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', bot=bot, + message = Message(0, None, self.group, from_user=user1, text='/start', bot=bot, entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))]) dp.process_update(Update(update_id=0, message=message)) @@ -1065,7 +1066,7 @@ class TestConversationHandler: self.raise_dp_handler_stop = True # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', bot=bot, + message = Message(0, None, self.group, text='/start', bot=bot, from_user=user1, entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start'))]) dp.process_update(Update(update_id=0, message=message)) diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index f124c09bf..3e7f75537 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -40,7 +40,8 @@ def dp2(bot): class TestDispatcher: message_update = Update(1, - message=Message(1, User(1, '', False), None, Chat(1, ''), text='Text')) + message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), + text='Text')) received = None count = 0 @@ -523,7 +524,8 @@ class TestDispatcher: # If updating a user_data or chat_data from a persistence object throws an error, # the error handler should catch it - update = Update(1, message=Message(1, User(1, "Test", False), None, Chat(1, "lala"), + update = Update(1, message=Message(1, None, Chat(1, "lala"), + from_user=User(1, "Test", False), text='/start', entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, @@ -638,7 +640,8 @@ class TestDispatcher: def logger(message): assert 'uncaught error was raised while handling' in message - update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), text='Text')) + update = Update(1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), + text='Text')) handler = MessageHandler(Filters.all, callback) cdp.add_handler(handler) cdp.add_error_handler(error) @@ -690,7 +693,8 @@ class TestDispatcher: cdp.add_handler(handler) cdp.persistence = OwnPersistence() - update = Update(1, message=Message(1, User(1, '', False), None, None, text='Text')) + update = Update(1, message=Message(1, None, None, from_user=User(1, '', False), + text='Text')) cdp.process_update(update) assert cdp.persistence.test_flag_bot_data assert cdp.persistence.test_flag_user_data @@ -699,7 +703,7 @@ class TestDispatcher: cdp.persistence.test_flag_bot_data = False cdp.persistence.test_flag_user_data = False cdp.persistence.test_flag_chat_data = False - update = Update(1, message=Message(1, None, None, Chat(1, ''), text='Text')) + update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) cdp.process_update(update) assert cdp.persistence.test_flag_bot_data assert not cdp.persistence.test_flag_user_data diff --git a/tests/test_error.py b/tests/test_error.py index 65ab8dbc0..791c86bb7 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -107,6 +107,7 @@ class TestErrors: ], ) def test_errors_pickling(self, exception, attributes): + print(exception) pickled = pickle.dumps(exception) unpickled = pickle.loads(pickled) assert type(unpickled) is type(exception) diff --git a/tests/test_filters.py b/tests/test_filters.py index d45a2441b..18555926c 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -27,8 +27,9 @@ import re @pytest.fixture(scope='function') def update(): - return Update(0, Message(0, User(0, 'Testuser', False), datetime.datetime.utcnow(), - Chat(0, 'private'), via_bot=User(0, "Testbot", True))) + return Update(0, Message(0, datetime.datetime.utcnow(), + Chat(0, 'private'), from_user=User(0, 'Testuser', False), + via_bot=User(0, "Testbot", True))) @pytest.fixture(scope='function', @@ -292,8 +293,8 @@ class TestFilters: assert result def test_filters_reply(self, update): - another_message = Message(1, User(1, 'TestOther', False), datetime.datetime.utcnow(), - Chat(0, 'private')) + another_message = Message(1, datetime.datetime.utcnow(), Chat(0, 'private'), + from_user=User(1, 'TestOther', False)) update.message.text = 'test' assert not Filters.reply(update) update.message.reply_to_message = another_message diff --git a/tests/test_inlinequeryhandler.py b/tests/test_inlinequeryhandler.py index f526aa37d..a647ece36 100644 --- a/tests/test_inlinequeryhandler.py +++ b/tests/test_inlinequeryhandler.py @@ -24,7 +24,7 @@ from telegram import (Update, CallbackQuery, Bot, Message, User, Chat, InlineQue ChosenInlineResult, ShippingQuery, PreCheckoutQuery, Location) from telegram.ext import InlineQueryHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index 5919f8544..944ba852d 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -121,7 +121,7 @@ class TestJobQueue: sleep(0.07) assert self.result == 1 - def test_run_repeating_last_timezone(self, job_queue, timezone): + def test_run_repeating_first_timezone(self, job_queue, timezone): """Test correct scheduling of job when passing a timezone-aware datetime as ``first``""" job_queue.run_repeating(self.job_run_once, 0.1, first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.05)) @@ -135,6 +135,15 @@ class TestJobQueue: sleep(0.1) assert self.result == 1 + def test_run_repeating_last_timezone(self, job_queue, timezone): + """Test correct scheduling of job when passing a timezone-aware datetime as ``first``""" + job_queue.run_repeating(self.job_run_once, 0.05, + last=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.06)) + sleep(0.1) + assert self.result == 1 + sleep(0.1) + assert self.result == 1 + def test_run_repeating_last_before_first(self, job_queue): with pytest.raises(ValueError, match="'last' must not be before 'first'!"): job_queue.run_repeating(self.job_run_once, 0.05, first=1, last=0.5) diff --git a/tests/test_message.py b/tests/test_message.py index d8a994388..a2be7c4bc 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -29,8 +29,8 @@ from tests.test_passport import RAW_PASSPORT_DATA @pytest.fixture(scope='class') def message(bot): - return Message(TestMessage.id_, TestMessage.from_user, TestMessage.date, TestMessage.chat, - bot=bot) + return Message(TestMessage.id_, TestMessage.date, TestMessage.chat, + from_user=TestMessage.from_user, bot=bot) @pytest.fixture(scope='function', @@ -888,10 +888,10 @@ class TestMessage: def test_equality(self): id_ = 1 - a = Message(id_, self.from_user, self.date, self.chat) - b = Message(id_, self.from_user, self.date, self.chat) - c = Message(id_, self.from_user, self.date, Chat(123, Chat.GROUP)) - d = Message(0, self.from_user, self.date, self.chat) + a = Message(id_, self.date, self.chat, from_user=self.from_user,) + b = Message(id_, self.date, self.chat, from_user=self.from_user,) + c = Message(id_, self.date, Chat(123, Chat.GROUP), from_user=User(0, '', False)) + d = Message(0, self.date, self.chat, from_user=self.from_user) e = Update(id_) assert a == b diff --git a/tests/test_messagehandler.py b/tests/test_messagehandler.py index 359289995..ecc8293c8 100644 --- a/tests/test_messagehandler.py +++ b/tests/test_messagehandler.py @@ -26,7 +26,7 @@ from telegram import (Message, Update, Chat, Bot, User, CallbackQuery, InlineQue ChosenInlineResult, ShippingQuery, PreCheckoutQuery) from telegram.ext import Filters, MessageHandler, CallbackContext, JobQueue, UpdateFilter -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)}, @@ -48,7 +48,7 @@ def false_update(request): @pytest.fixture(scope='class') def message(bot): - return Message(1, User(1, '', False), None, Chat(1, ''), bot=bot) + return Message(1, None, Chat(1, ''), from_user=User(1, '', False), bot=bot) class TestMessageHandler: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 03ab6e844..7141bf304 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -242,7 +242,7 @@ class TestBasePersistence: 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, user1, None, chat2) + m = Message(1, None, chat2, from_user=user1) u = Update(0, m) with caplog.at_level(logging.ERROR): dp.process_update(u) @@ -338,7 +338,7 @@ class TestBasePersistence: 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, user1, None, chat2) + m = Message(1, None, chat2, from_user=user1) u = Update(0, m) with caplog.at_level(logging.ERROR): cdp.process_update(u) @@ -585,7 +585,7 @@ def pickle_files_wo_bot_data(user_data, chat_data, conversations): def update(bot): user = User(id=321, first_name='test_user', is_bot=False) chat = Chat(id=123, type='group') - message = Message(1, user, None, chat, text="Hi there", bot=bot) + message = Message(1, None, chat, from_user=user, text="Hi there", bot=bot) return Update(0, message=message) diff --git a/tests/test_pollanswerhandler.py b/tests/test_pollanswerhandler.py index 09b839291..1c90d7f76 100644 --- a/tests/test_pollanswerhandler.py +++ b/tests/test_pollanswerhandler.py @@ -24,7 +24,7 @@ from telegram import (Update, CallbackQuery, Bot, Message, User, Chat, PollAnswe ChosenInlineResult, ShippingQuery, PreCheckoutQuery) from telegram.ext import PollAnswerHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_pollhandler.py b/tests/test_pollhandler.py index 6c09dd47d..033c59f56 100644 --- a/tests/test_pollhandler.py +++ b/tests/test_pollhandler.py @@ -24,7 +24,7 @@ from telegram import (Update, Poll, PollOption, Bot, Message, User, Chat, Callba ChosenInlineResult, ShippingQuery, PreCheckoutQuery) from telegram.ext import PollHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_precheckoutqueryhandler.py b/tests/test_precheckoutqueryhandler.py index 2e2e922a2..de1172d8e 100644 --- a/tests/test_precheckoutqueryhandler.py +++ b/tests/test_precheckoutqueryhandler.py @@ -24,7 +24,7 @@ from telegram import (Update, Chat, Bot, ChosenInlineResult, User, Message, Call InlineQuery, ShippingQuery, PreCheckoutQuery) from telegram.ext import PreCheckoutQueryHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_regexhandler.py b/tests/test_regexhandler.py index 5b7a75eb2..992f87f00 100644 --- a/tests/test_regexhandler.py +++ b/tests/test_regexhandler.py @@ -25,7 +25,7 @@ from telegram import (Message, Update, Chat, Bot, User, CallbackQuery, InlineQue ChosenInlineResult, ShippingQuery, PreCheckoutQuery) from telegram.ext import RegexHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)}, @@ -47,7 +47,8 @@ def false_update(request): @pytest.fixture(scope='class') def message(bot): - return Message(1, User(1, '', False), None, Chat(1, ''), text='test message', bot=bot) + return Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='test message', + bot=bot) class TestRegexHandler: diff --git a/tests/test_shippingqueryhandler.py b/tests/test_shippingqueryhandler.py index 676c7b603..daccaa3a4 100644 --- a/tests/test_shippingqueryhandler.py +++ b/tests/test_shippingqueryhandler.py @@ -24,7 +24,7 @@ from telegram import (Update, Chat, Bot, ChosenInlineResult, User, Message, Call InlineQuery, ShippingQuery, PreCheckoutQuery, ShippingAddress) from telegram.ext import ShippingQueryHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_stringcommandhandler.py b/tests/test_stringcommandhandler.py index 5bd877949..e1a0e3305 100644 --- a/tests/test_stringcommandhandler.py +++ b/tests/test_stringcommandhandler.py @@ -24,7 +24,7 @@ from telegram import (Bot, Update, Message, User, Chat, CallbackQuery, InlineQue ChosenInlineResult, ShippingQuery, PreCheckoutQuery) from telegram.ext import StringCommandHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_stringregexhandler.py b/tests/test_stringregexhandler.py index cd6fb23fd..e86bcef79 100644 --- a/tests/test_stringregexhandler.py +++ b/tests/test_stringregexhandler.py @@ -24,7 +24,7 @@ from telegram import (Bot, Update, Message, User, Chat, CallbackQuery, InlineQue ChosenInlineResult, ShippingQuery, PreCheckoutQuery) from telegram.ext import StringRegexHandler, CallbackContext, JobQueue -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_update.py b/tests/test_update.py index 196f355e6..2b69b4db7 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -23,7 +23,7 @@ from telegram import (Message, User, Update, Chat, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, Poll, PollOption) from telegram.poll import PollAnswer -message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text') +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') params = [ {'message': message}, diff --git a/tests/test_updater.py b/tests/test_updater.py index 939ea4da3..b484c802f 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -174,7 +174,7 @@ class TestUpdater: sleep(.2) try: # Now, we send an update to the server via urlopen - update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), + update = Update(1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook')) self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN') sleep(.2) @@ -331,7 +331,7 @@ class TestUpdater: sleep(.2) # Now, we send an update to the server via urlopen - update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), + update = Update(1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook 2')) self._send_webhook_msg(ip, port, update.to_json()) sleep(.2)