Merge branch 'master' into overhaul-tests

This commit is contained in:
Harshil 2024-05-22 16:53:12 -04:00
commit b768001c5a
No known key found for this signature in database
GPG key ID: 4AC061E441A1F727
81 changed files with 3437 additions and 396 deletions

View file

@ -157,7 +157,7 @@ Check-list for PRs
This checklist is a non-exhaustive reminder of things that should be done before a PR is merged, both for you as contributor and for the maintainers.
Feel free to copy (parts of) the checklist to the PR description to remind you or the maintainers of open points or if you have questions on anything.
- Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION`` or ``.. deprecated:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
- Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION``, ``.. deprecated:: NEXT.VERSION`` or ``.. versionremoved:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
- Created new or adapted existing unit tests
- Documented code changes according to the `CSI standard <https://standards.mousepawmedia.com/en/stable/csi.html>`__
- Added myself alphabetically to ``AUTHORS.rst`` (optional)
@ -276,7 +276,7 @@ This gives us the flexibility to re-order arguments and more importantly
to add new required arguments. It's also more explicit and easier to read.
.. _`Code of Conduct`: https://www.python.org/psf/conduct/
.. _`Code of Conduct`: https://policies.python.org/python.org/code-of-conduct/
.. _`issue tracker`: https://github.com/python-telegram-bot/python-telegram-bot/issues
.. _`Telegram group`: https://telegram.me/pythontelegrambotgroup
.. _`PEP 8 Style Guide`: https://peps.python.org/pep-0008/

View file

@ -12,6 +12,8 @@ body:
To make it easier for us to help you, please read this [article](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Ask-Right).
Please mind that there is also a users' [Telegram group](https://t.me/pythontelegrambotgroup) for questions about the library. Questions asked there might be answered quicker than here. Moreover, [GitHub Discussions](https://github.com/python-telegram-bot/python-telegram-bot/discussions) offer a slightly better format to discuss usage questions.
If you have asked the same question elsewhere (e.g. the [Telegram group](https://t.me/pythontelegrambotgroup) or [StackOverflow](https://stackoverflow.com/questions/tagged/python-telegram-bot)), provide a link to that thread.
- type: textarea
id: issue-faced

View file

@ -2,7 +2,8 @@
ci:
autofix_prs: false
autoupdate_schedule: monthly
autoupdate_schedule: quarterly
autoupdate_commit_msg: 'Bump `pre-commit` Hooks to Latest Versions'
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit

View file

@ -4,6 +4,58 @@
Changelog
=========
Version 21.2
============
*Released 2024-05-20*
This is the technical changelog for version 21.2. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
Major Changes
-------------
- Full Support for Bot API 7.3 (:pr:`4246`, :pr:`4260`, :pr:`4243`, :pr:`4248`, :pr:`4242` closes :issue:`4236`, :pr:`4247` by `aelkheir <https://github.com/aelkheir>`_)
- Remove Functionality Deprecated by Bot API 7.2 (:pr:`4245`)
New Features
------------
- Add Version to ``PTBDeprecationWarning`` (:pr:`4262` closes :issue:`4261`)
- Handle Exceptions in building ``CallbackContext`` (:pr:`4222`)
Bug Fixes
---------
- Call ``Application.post_stop`` Only if ``Application.stop`` was called (:pr:`4211` closes :issue:`4210`)
- Handle ``SystemExit`` raised in Handlers (:pr:`4157` closes :issue:`4155` and :issue:`4156`)
- Make ``Birthdate.to_date`` Return a ``datetime.date`` Object (:pr:`4251`)
Documentation Improvements
--------------------------
- Documentation Improvements (:pr:`4217`)
Internal Changes
----------------
- Add New Rules to ``ruff`` Config (:pr:`4250`)
- Adapt Test Suite to Changes in Error Messages (:pr:`4238`)
Dependency Updates
------------------
- Bump ``furo`` from 2024.4.27 to 2024.5.6 (:pr:`4252`)
- ``pre-commit`` autoupdate (:pr:`4239`)
- Bump ``pytest`` from 8.1.1 to 8.2.0 (:pr:`4231`)
- Bump ``dependabot/fetch-metadata`` from 2.0.0 to 2.1.0 (:pr:`4228`)
- Bump ``pytest-asyncio`` from 0.21.1 to 0.21.2 (:pr:`4232`)
- Bump ``pytest-xdist`` from 3.6.0 to 3.6.1 (:pr:`4233`)
- Bump ``furo`` from 2024.1.29 to 2024.4.27 (:pr:`4230`)
- Bump ``srvaroa/labeler`` from 1.10.0 to 1.10.1 (:pr:`4227`)
- Bump ``pytest`` from 7.4.4 to 8.1.1 (:pr:`4218`)
- Bump ``sphinx`` from 7.2.6 to 7.3.7 (:pr:`4215`)
- Bump ``pytest-xdist`` from 3.5.0 to 3.6.0 (:pr:`4215`)
Version 21.1.1
==============

View file

@ -14,9 +14,9 @@
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-7.3-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
:alt: Supported Bot API version
.. image:: https://img.shields.io/pypi/dm/python-telegram-bot
:target: https://pypistats.org/packages/python-telegram-bot
@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support
====================
All types and methods of the Telegram Bot API **7.2** are supported.
All types and methods of the Telegram Bot API **7.3** are supported.
Installing
==========

View file

@ -14,9 +14,9 @@
:target: https://pypi.org/project/python-telegram-bot-raw/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-7.3-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
:alt: Supported Bot API version
.. image:: https://img.shields.io/pypi/dm/python-telegram-bot-raw
:target: https://pypistats.org/packages/python-telegram-bot-raw
@ -85,7 +85,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support
====================
All types and methods of the Telegram Bot API **7.2** are supported.
All types and methods of the Telegram Bot API **7.3** are supported.
Installing
==========

View file

@ -20,9 +20,9 @@ author = "Leandro Toledo"
# built documents.
#
# The short X.Y version.
version = "21.1.1" # telegram.__version__[:3]
version = "21.2" # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = "21.1.1" # telegram.__version__
release = "21.2" # telegram.__version__
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "6.1.3"
@ -67,7 +67,9 @@ source_suffix = ".rst"
master_doc = "index"
# Global substitutions
rst_prolog = (Path.cwd() / "../substitutions/global.rst").read_text(encoding="utf-8")
rst_prolog = ""
for file in Path.cwd().glob("../substitutions/*.rst"):
rst_prolog += "\n" + file.read_text(encoding="utf-8")
# -- Extension settings ------------------------------------------------
napoleon_use_admonition_for_examples = True

View file

@ -28,6 +28,16 @@ Available Types
telegram.callbackquery
telegram.chat
telegram.chatadministratorrights
telegram.chatbackground
telegram.backgroundtype
telegram.backgroundtypefill
telegram.backgroundtypewallpaper
telegram.backgroundtypepattern
telegram.backgroundtypechattheme
telegram.backgroundfill
telegram.backgroundfillsolid
telegram.backgroundfillgradient
telegram.backgroundfillfreeformgradient
telegram.chatboost
telegram.chatboostadded
telegram.chatboostremoved
@ -36,6 +46,7 @@ Available Types
telegram.chatboostsourcegiveaway
telegram.chatboostsourcepremium
telegram.chatboostupdated
telegram.chatfullinfo
telegram.chatinvitelink
telegram.chatjoinrequest
telegram.chatlocation
@ -77,6 +88,7 @@ Available Types
telegram.inputmediadocument
telegram.inputmediaphoto
telegram.inputmediavideo
telegram.inputpolloption
telegram.inputsticker
telegram.keyboardbutton
telegram.keyboardbuttonpolltype

View file

@ -0,0 +1,8 @@
BackgroundFill
==============
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundFill
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundFillFreeformGradient
==============================
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundFillFreeformGradient
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundFillGradient
======================
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundFillGradient
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundFillSolid
===================
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundFillSolid
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundType
==============
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundType
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundTypeChatTheme
=======================
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundTypeChatTheme
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundTypeFill
==================
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundTypeFill
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundTypePattern
=====================
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundTypePattern
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
BackgroundTypeWallpaper
=======================
.. versionadded:: 21.2
.. autoclass:: telegram.BackgroundTypeWallpaper
:members:
:show-inheritance:

View file

@ -0,0 +1,8 @@
ChatBackground
==============
.. versionadded:: 21.2
.. autoclass:: telegram.ChatBackground
:members:
:show-inheritance:

View file

@ -0,0 +1,6 @@
ChatFullInfo
============
.. autoclass:: telegram.ChatFullInfo
:members:
:show-inheritance:

View file

@ -0,0 +1,6 @@
InputPollOption
===============
.. autoclass:: telegram.InputPollOption
:members:
:show-inheritance:

View file

@ -0,0 +1 @@
.. |app_run_shutdown| replace:: The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised. This also works from within handlers, error handlers and jobs. However, using :meth:`~telegram.ext.Application.stop_running` will give a somewhat cleaner shutdown behavior than manually raising those exceptions. On unix, the app will also shut down on receiving the signals specified by

View file

@ -22,6 +22,15 @@ __author__ = "devs@python-telegram-bot.org"
__all__ = (
"Animation",
"Audio",
"BackgroundFill",
"BackgroundFillFreeformGradient",
"BackgroundFillGradient",
"BackgroundFillSolid",
"BackgroundType",
"BackgroundTypeChatTheme",
"BackgroundTypeFill",
"BackgroundTypePattern",
"BackgroundTypeWallpaper",
"Birthdate",
"Bot",
"BotCommand",
@ -46,6 +55,7 @@ __all__ = (
"CallbackQuery",
"Chat",
"ChatAdministratorRights",
"ChatBackground",
"ChatBoost",
"ChatBoostAdded",
"ChatBoostRemoved",
@ -54,6 +64,7 @@ __all__ = (
"ChatBoostSourceGiveaway",
"ChatBoostSourcePremium",
"ChatBoostUpdated",
"ChatFullInfo",
"ChatInviteLink",
"ChatJoinRequest",
"ChatLocation",
@ -131,6 +142,7 @@ __all__ = (
"InputMediaPhoto",
"InputMediaVideo",
"InputMessageContent",
"InputPollOption",
"InputSticker",
"InputTextMessageContent",
"InputVenueMessageContent",
@ -258,6 +270,18 @@ from ._business import (
from ._callbackquery import CallbackQuery
from ._chat import Chat
from ._chatadministratorrights import ChatAdministratorRights
from ._chatbackground import (
BackgroundFill,
BackgroundFillFreeformGradient,
BackgroundFillGradient,
BackgroundFillSolid,
BackgroundType,
BackgroundTypeChatTheme,
BackgroundTypeFill,
BackgroundTypePattern,
BackgroundTypeWallpaper,
ChatBackground,
)
from ._chatboost import (
ChatBoost,
ChatBoostAdded,
@ -269,6 +293,7 @@ from ._chatboost import (
ChatBoostUpdated,
UserChatBoosts,
)
from ._chatfullinfo import ChatFullInfo
from ._chatinvitelink import ChatInviteLink
from ._chatjoinrequest import ChatJoinRequest
from ._chatlocation import ChatLocation
@ -403,7 +428,7 @@ from ._payment.shippingaddress import ShippingAddress
from ._payment.shippingoption import ShippingOption
from ._payment.shippingquery import ShippingQuery
from ._payment.successfulpayment import SuccessfulPayment
from ._poll import Poll, PollAnswer, PollOption
from ._poll import InputPollOption, Poll, PollAnswer, PollOption
from ._proximityalerttriggered import ProximityAlertTriggered
from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote

View file

@ -26,7 +26,7 @@ from telegram._utils.types import JSONDict
class Birthdate(TelegramObject):
"""
This object represents a user's birthday.
This object describes the birthdate of a user.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`day`, and :attr:`month` are equal.
@ -71,9 +71,9 @@ class Birthdate(TelegramObject):
self._freeze()
def to_date(self, year: Optional[int] = None) -> date:
"""Return the birthdate as a datetime object.
"""Return the birthdate as a date object.
.. versionchanged:: NEXT.VERSION
.. versionchanged:: 21.2
Now returns a :obj:`datetime.date` object instead of a :obj:`datetime.datetime` object,
as was originally intended.

View file

@ -58,9 +58,9 @@ from telegram._botcommandscope import BotCommandScope
from telegram._botdescription import BotDescription, BotShortDescription
from telegram._botname import BotName
from telegram._business import BusinessConnection
from telegram._chat import Chat
from telegram._chatadministratorrights import ChatAdministratorRights
from telegram._chatboost import UserChatBoosts
from telegram._chatfullinfo import ChatFullInfo
from telegram._chatinvitelink import ChatInviteLink
from telegram._chatmember import ChatMember
from telegram._chatpermissions import ChatPermissions
@ -84,7 +84,7 @@ from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
from telegram._menubutton import MenuButton
from telegram._message import Message
from telegram._messageid import MessageId
from telegram._poll import Poll
from telegram._poll import InputPollOption, Poll
from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
from telegram._reply import ReplyParameters
from telegram._sentwebappmessage import SentWebAppMessage
@ -524,7 +524,10 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
@classmethod
def _warn(
cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0
cls,
message: Union[str, PTBUserWarning],
category: Type[Warning] = PTBUserWarning,
stacklevel: int = 0,
) -> None:
"""Convenience method to issue a warning. This method is here mostly to make it easier
for ExtBot to add 1 level to all warning calls.
@ -837,7 +840,6 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
f"Please use 'Bot.{endpoint}' instead of "
f"'Bot.do_api_request(\"{endpoint}\", ...)'"
),
PTBDeprecationWarning,
stacklevel=2,
)
@ -2287,9 +2289,10 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
"""
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
encoded with OPUS (other formats may be sent as Audio or Document). Bots can currently
send voice messages of up to :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD`
in size, this limit may be changed in the future.
encoded with OPUS , or in .MP3 format, or in .M4A format (other formats may be sent as
:class:`~telegram.Audio` or :class:`~telegram.Document`). Bots can currently send voice
messages of up to :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size,
this limit may be changed in the future.
Note:
To use this method, the file must have the type :mimetype:`audio/ogg` and be no more
@ -2610,7 +2613,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
live_period (:obj:`int`, optional): Period in seconds for which the location will be
updated, should be between
:tg-const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` and
:tg-const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD`.
:tg-const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD`, or
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
locations that can be edited indefinitely.
heading (:obj:`int`, optional): For live locations, a direction in which the user is
moving, in degrees. Must be between
:tg-const:`telegram.constants.LocationLimit.MIN_HEADING` and
@ -2720,6 +2725,7 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
live_period: Optional[int] = None,
*,
location: Optional[Location] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -2758,6 +2764,15 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
if specified.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new
inline keyboard.
live_period (:obj:`int`, optional): New period in seconds during which the location
can be updated, starting from the message send date. If
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` is specified,
then the location can be updated forever. Otherwise, the new value must not exceed
the current ``live_period`` by more than a day, and the live location expiration
date must remain within the next 90 days. If not specified, then ``live_period``
remains unchanged
.. versionadded:: 21.2.
Keyword Args:
location (:class:`telegram.Location`, optional): The location to send.
@ -2790,6 +2805,7 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
"horizontal_accuracy": horizontal_accuracy,
"heading": heading,
"proximity_alert_radius": proximity_alert_radius,
"live_period": live_period,
}
return await self._send_message(
@ -4195,10 +4211,12 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
except NotImplementedError:
arg_read_timeout = 2
self._warn(
f"The class {self._request[0].__class__.__name__} does not override "
"the property `read_timeout`. Overriding this property will be mandatory in "
"future versions. Using 2 seconds as fallback.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.7",
f"The class {self._request[0].__class__.__name__} does not override "
"the property `read_timeout`. Overriding this property will be mandatory "
"in future versions. Using 2 seconds as fallback.",
),
stacklevel=2,
)
@ -4434,16 +4452,19 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Chat:
) -> ChatFullInfo:
"""
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.).
.. versionchanged:: 21.2
In accordance to Bot API 7.3, this method now returns a :class:`telegram.ChatFullInfo`.
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
Returns:
:class:`telegram.Chat`
:class:`telegram.ChatFullInfo`
Raises:
:class:`telegram.error.TelegramError`
@ -4461,7 +4482,7 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
api_kwargs=api_kwargs,
)
return Chat.de_json(result, self) # type: ignore[return-value]
return ChatFullInfo.de_json(result, self) # type: ignore[return-value]
async def get_chat_administrators(
self,
@ -5311,7 +5332,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
.. versionadded:: 20.6
can_edit_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
edit stories posted by other users.
edit stories posted by other users, post stories to the chat page, pin chat
stories, and access the chat's story archive
.. versionadded:: 20.6
can_delete_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
@ -6338,7 +6360,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Removed the deprecated parameters mentioned above and adjusted the order of the
parameters.
.. versionremoved:: NEXT.VERSION
.. versionremoved:: 21.2
Removed the deprecated parameter ``sticker_format``.
Args:
@ -6798,7 +6820,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
self,
chat_id: Union[int, str],
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None, # pylint: disable=redefined-builtin
allows_multiple_answers: Optional[bool] = None,
@ -6815,6 +6837,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@ -6831,14 +6855,20 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`-
:tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters.
options (Sequence[:obj:`str`]): Sequence of answer options,
options (Sequence[:obj:`str` | :class:`telegram.InputPollOption`]): Sequence of
:tg-const:`telegram.Poll.MIN_OPTION_NUMBER`-
:tg-const:`telegram.Poll.MAX_OPTION_NUMBER` strings
:tg-const:`telegram.Poll.MAX_OPTION_NUMBER` answer options. Each option may either
be a string with
:tg-const:`telegram.Poll.MIN_OPTION_LENGTH`-
:tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters each.
:tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters or an
:class:`~telegram.InputPollOption` object. Strings are converted to
:class:`~telegram.InputPollOption` objects automatically.
.. versionchanged:: 20.0
|sequenceargs|
.. versionchanged:: 21.2
Bot API 7.3 adds support for :class:`~telegram.InputPollOption` objects.
is_anonymous (:obj:`bool`, optional): :obj:`True`, if the poll needs to be anonymous,
defaults to :obj:`True`.
type (:obj:`str`, optional): Poll type, :tg-const:`telegram.Poll.QUIZ` or
@ -6893,6 +6923,16 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
business_connection_id (:obj:`str`, optional): |business_id_str|
.. versionadded:: 21.1
question_parse_mode (:obj:`str`, optional): Mode for parsing entities in the question.
See the constants in :class:`telegram.constants.ParseMode` for the available modes.
Currently, only custom emoji entities are allowed.
.. versionadded:: 21.2
question_entities (Sequence[:class:`telegram.Message`], optional): Special entities
that appear in the poll :paramref:`question`. It can be specified instead of
:paramref:`question_parse_mode`.
.. versionadded:: 21.2
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@ -6924,7 +6964,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
data: JSONDict = {
"chat_id": chat_id,
"question": question,
"options": options,
"options": [
InputPollOption(option) if isinstance(option, str) else option
for option in options
],
"explanation_parse_mode": explanation_parse_mode,
"is_anonymous": is_anonymous,
"type": type,
@ -6935,6 +6978,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"explanation_entities": explanation_entities,
"open_period": open_period,
"close_date": close_date,
"question_parse_mode": question_parse_mode,
"question_entities": question_entities,
}
return await self._send_message(

View file

@ -189,7 +189,7 @@ class BusinessMessagesDeleted(TelegramObject):
class BusinessIntro(TelegramObject):
"""
This object represents the intro of a business account.
This object contains information about the start page settings of a Telegram Business account.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their
@ -246,7 +246,7 @@ class BusinessIntro(TelegramObject):
class BusinessLocation(TelegramObject):
"""
This object represents the location of a business account.
This object contains information about the location of a Telegram Business account.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their
@ -298,7 +298,7 @@ class BusinessLocation(TelegramObject):
class BusinessOpeningHoursInterval(TelegramObject):
"""
This object represents the time intervals describing business opening hours.
This object describes an interval of time during which a business is open.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their
@ -390,7 +390,7 @@ class BusinessOpeningHoursInterval(TelegramObject):
class BusinessOpeningHours(TelegramObject):
"""
This object represents the opening hours of a business account.
This object describes the opening hours of a business.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their

View file

@ -461,6 +461,7 @@ class CallbackQuery(TelegramObject):
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
live_period: Optional[int] = None,
*,
location: Optional[Location] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -509,6 +510,7 @@ class CallbackQuery(TelegramObject):
horizontal_accuracy=horizontal_accuracy,
heading=heading,
proximity_alert_radius=proximity_alert_radius,
live_period=live_period,
chat_id=None,
message_id=None,
)
@ -525,6 +527,7 @@ class CallbackQuery(TelegramObject):
horizontal_accuracy=horizontal_accuracy,
heading=heading,
proximity_alert_radius=proximity_alert_radius,
live_period=live_period,
)
async def stop_message_live_location(

View file

@ -20,7 +20,7 @@
"""This module contains an object that represents a Telegram Chat."""
from datetime import datetime
from html import escape
from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union
from typing import TYPE_CHECKING, Any, Final, Optional, Sequence, Tuple, Union
from telegram import constants
from telegram._birthdate import Birthdate
@ -36,9 +36,11 @@ from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
from telegram._utils.warnings import warn
from telegram.helpers import escape_markdown
from telegram.helpers import mention_html as helpers_mention_html
from telegram.helpers import mention_markdown as helpers_mention_markdown
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import (
@ -57,6 +59,7 @@ if TYPE_CHECKING:
InputMediaDocument,
InputMediaPhoto,
InputMediaVideo,
InputPollOption,
LabeledPrice,
LinkPreviewOptions,
Location,
@ -74,6 +77,45 @@ if TYPE_CHECKING:
)
_deprecated_attrs = (
"accent_color_id",
"active_usernames",
"available_reactions",
"background_custom_emoji_id",
"bio",
"birthdate",
"business_intro",
"business_location",
"business_opening_hours",
"can_set_sticker_set",
"custom_emoji_sticker_set_name",
"description",
"emoji_status_custom_emoji_id",
"emoji_status_expiration_date",
"has_aggressive_anti_spam_enabled",
"has_hidden_members",
"has_private_forwards",
"has_protected_content",
"has_restricted_voice_and_video_messages",
"has_visible_history",
"invite_link",
"join_by_request",
"join_to_send_messages",
"linked_chat_id",
"location",
"message_auto_delete_time",
"permissions",
"personal_chat",
"photo",
"pinned_message",
"profile_accent_color_id",
"profile_background_custom_emoji_id",
"slow_mode_delay",
"sticker_set_name",
"unrestrict_boost_count",
)
class Chat(TelegramObject):
"""This object represents a chat.
@ -107,62 +149,134 @@ class Chat(TelegramObject):
last_name (:obj:`str`, optional): Last name of the other party in a private chat.
photo (:class:`telegram.ChatPhoto`, optional): Chat photo.
Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_private_forwards (:obj:`bool`, optional): :obj:`True`, if privacy settings of the other
party in the private chat allows to use ``tg://user?id=<user_id>`` links only in chats
with the user. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.9
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
description (:obj:`str`, optional): Description, for groups, supergroups and channel chats.
Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and
channel. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
pinned_message (:class:`telegram.Message`, optional): The most recent pinned message
(by sending date). Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between
consecutive messages sent by each unprivileged user.
Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to
the chat will be automatically deleted; in seconds. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.4
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't
be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.9
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_visible_history (:obj:`bool`, optional): :obj:`True`, if new chat members will have
access to old messages; available only to chat administrators. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set.
Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the
sticker set. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
linked_chat_id (:obj:`int`, optional): Unique identifier for the linked chat, i.e. the
discussion group identifier for a channel and vice versa; for supergroups and channel
chats. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
join_to_send_messages (:obj:`bool`, optional): :obj:`True`, if users need to join the
supergroup before they can send messages. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
join_by_request (:obj:`bool`, optional): :obj:`True`, if all users directly joining the
supergroup need to be approved by supergroup administrators. Returned only in
:meth:`telegram.Bot.get_chat`.
supergroup without using an invite link need to be approved by supergroup
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_restricted_voice_and_video_messages (:obj:`bool`, optional): :obj:`True`, if the
privacy settings of the other party restrict sending voice and video note messages
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum
(has topics_ enabled).
@ -173,27 +287,47 @@ class Chat(TelegramObject):
only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
business_intro (:class:`telegram.BusinessIntro`, optional): For private chats with
business accounts, the intro of the business. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
business_location (:class:`telegram.BusinessLocation`, optional): For private chats with
business accounts, the location of the business. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
business_opening_hours (:class:`telegram.BusinessOpeningHours`, optional): For private
chats with business accounts, the opening hours of the business. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
available_reactions (Sequence[:class:`telegram.ReactionType`], optional): List of available
reactions allowed in the chat. If omitted, then all of
:const:`telegram.constants.ReactionEmoji` are allowed. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
accent_color_id (:obj:`int`, optional): Identifier of the
:class:`accent color <telegram.constants.AccentColor>` for the chat name and
backgrounds of the chat photo, reply header, and link preview. See `accent colors`_
@ -201,62 +335,110 @@ class Chat(TelegramObject):
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
background_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji chosen
by the chat for the reply header and link preview background. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
profile_accent_color_id (:obj:`int`, optional): Identifier of the
:class:`accent color <telegram.constants.ProfileAccentColor>` for the chat's profile
background. See profile `accent colors`_ for more details. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
profile_background_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of
the emoji chosen by the chat for its profile background. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji
status of the chat or the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
emoji_status_expiration_date (:class:`datetime.datetime`, optional): Expiration date of
emoji status of the chat or the other party in a private chat, in seconds. Returned
only in :meth:`telegram.Bot.get_chat`.
|datetime_localization|
.. versionadded:: 20.5
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_aggressive_anti_spam_enabled (:obj:`bool`, optional): :obj:`True`, if aggressive
anti-spam checks are enabled in the supergroup. The field is only available to chat
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_hidden_members (:obj:`bool`, optional): :obj:`True`, if non-administrators can only
get the list of bots and administrators in the chat. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
unrestrict_boost_count (:obj:`int`, optional): For supergroups, the minimum number of
boosts that a non-administrator user needs to add in order to ignore slow mode and chat
permissions. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
custom_emoji_sticker_set_name (:obj:`str`, optional): For supergroups, the name of the
group's custom emoji sticker set. Custom emoji from this set can be used by all users
and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
birthdate (:obj:`telegram.Birthdate`, optional): For private chats,
the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
personal_chat (:obj:`telegram.Chat`, optional): For private chats, the personal channel of
the user. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
Attributes:
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits
and some programming languages may have difficulty/silent defects in interpreting it.
@ -271,62 +453,134 @@ class Chat(TelegramObject):
last_name (:obj:`str`): Optional. Last name of the other party in a private chat.
photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_private_forwards (:obj:`bool`): Optional. :obj:`True`, if privacy settings of the other
party in the private chat allows to use ``tg://user?id=<user_id>`` links only in chats
with the user. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.9
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats.
Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and
channel. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message
(by sending date). Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between
consecutive messages sent by each unprivileged user. Returned only in
:meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to
the chat will be automatically deleted; in seconds. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.4
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't
be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.9
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_visible_history (:obj:`bool`): Optional. :obj:`True`, if new chat members will have
access to old messages; available only to chat administrators. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the
sticker set. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
linked_chat_id (:obj:`int`): Optional. Unique identifier for the linked chat, i.e. the
discussion group identifier for a channel and vice versa; for supergroups and channel
chats. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
join_to_send_messages (:obj:`bool`): Optional. :obj:`True`, if users need to join
the supergroup before they can send messages. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly
joining the supergroup need to be approved by supergroup administrators. Returned only
in :meth:`telegram.Bot.get_chat`.
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly joining the
supergroup without using an invite link need to be approved by supergroup
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_restricted_voice_and_video_messages (:obj:`bool`): Optional. :obj:`True`, if the
privacy settings of the other party restrict sending voice and video note messages
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum
(has topics_ enabled).
@ -339,27 +593,47 @@ class Chat(TelegramObject):
obtained via :meth:`~telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
business_intro (:class:`telegram.BusinessIntro`): Optional. For private chats with
business accounts, the intro of the business. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
business_location (:class:`telegram.BusinessLocation`): Optional. For private chats with
business accounts, the location of the business. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
business_opening_hours (:class:`telegram.BusinessOpeningHours`): Optional. For private
chats with business accounts, the opening hours of the business. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
available_reactions (Tuple[:class:`telegram.ReactionType`]): Optional. List of available
reactions allowed in the chat. If omitted, then all of
:const:`telegram.constants.ReactionEmoji` are allowed. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
accent_color_id (:obj:`int`): Optional. Identifier of the
:class:`accent color <telegram.constants.AccentColor>` for the chat name and
backgrounds of the chat photo, reply header, and link preview. See `accent colors`_
@ -367,62 +641,110 @@ class Chat(TelegramObject):
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
background_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji chosen
by the chat for the reply header and link preview background. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
profile_accent_color_id (:obj:`int`): Optional. Identifier of the
:class:`accent color <telegram.constants.ProfileAccentColor>` for the chat's profile
background. See profile `accent colors`_ for more details. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
profile_background_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of
the emoji chosen by the chat for its profile background. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.8
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
emoji_status_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji
status of the chat or the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
emoji_status_expiration_date (:class:`datetime.datetime`): Optional. Expiration date of
emoji status of the chat or the other party in a private chat, in seconds. Returned
only in :meth:`telegram.Bot.get_chat`.
|datetime_localization|
.. versionadded:: 20.5
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_aggressive_anti_spam_enabled (:obj:`bool`): Optional. :obj:`True`, if aggressive
anti-spam checks are enabled in the supergroup. The field is only available to chat
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
has_hidden_members (:obj:`bool`): Optional. :obj:`True`, if non-administrators can only
get the list of bots and administrators in the chat. Returned only in
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
unrestrict_boost_count (:obj:`int`): Optional. For supergroups, the minimum number of
boosts that a non-administrator user needs to add in order to ignore slow mode and chat
permissions. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
custom_emoji_sticker_set_name (:obj:`str`): Optional. For supergroups, the name of the
group's custom emoji sticker set. Custom emoji from this set can be used by all users
and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.0
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
birthdate (:obj:`telegram.Birthdate`): Optional. For private chats,
the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
personal_chat (:obj:`telegram.Chat`): Optional. For private chats, the personal channel of
the user. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.1
.. deprecated:: 21.2
In accordance to Bot API 7.3, this attribute will be moved to
:class:`telegram.ChatFullInfo`.
.. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
.. _accent colors: https://core.telegram.org/bots/api#accent-colors
"""
@ -471,7 +793,6 @@ class Chat(TelegramObject):
"unrestrict_boost_count",
"username",
)
SENDER: Final[str] = constants.ChatType.SENDER
""":const:`telegram.constants.ChatType.SENDER`
@ -518,7 +839,7 @@ class Chat(TelegramObject):
has_aggressive_anti_spam_enabled: Optional[bool] = None,
has_hidden_members: Optional[bool] = None,
available_reactions: Optional[Sequence[ReactionType]] = None,
accent_color_id: Optional[int] = None,
accent_color_id: Optional[int] = None, # required in API 7.3 - Optional for back compat
background_custom_emoji_id: Optional[str] = None,
profile_accent_color_id: Optional[int] = None,
profile_background_custom_emoji_id: Optional[str] = None,
@ -585,10 +906,34 @@ class Chat(TelegramObject):
self.business_location: Optional["BusinessLocation"] = business_location
self.business_opening_hours: Optional["BusinessOpeningHours"] = business_opening_hours
if self.__class__ is Chat:
for arg in _deprecated_attrs:
if (val := object.__getattribute__(self, arg)) is not None and val != ():
warn(
PTBDeprecationWarning(
"21.2",
f"The argument `{arg}` is deprecated and will only be available via "
"`ChatFullInfo` in the future.",
),
stacklevel=2,
)
self._id_attrs = (self.id,)
self._freeze()
def __getattribute__(self, name: str) -> Any:
if name in _deprecated_attrs and self.__class__ is Chat:
warn(
PTBDeprecationWarning(
"21.2",
f"The attribute `{name}` is deprecated and will only be accessible via "
"`ChatFullInfo` in the future.",
),
stacklevel=2,
)
return super().__getattribute__(name)
@property
def effective_name(self) -> Optional[str]:
"""
@ -658,7 +1003,7 @@ class Chat(TelegramObject):
data["location"] = ChatLocation.de_json(data.get("location"), bot)
data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot)
data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot)
data["personal_chat"] = cls.de_json(data.get("personal_chat"), bot)
data["personal_chat"] = Chat.de_json(data.get("personal_chat"), bot)
data["business_intro"] = BusinessIntro.de_json(data.get("business_intro"), bot)
data["business_location"] = BusinessLocation.de_json(data.get("business_location"), bot)
data["business_opening_hours"] = BusinessOpeningHours.de_json(
@ -2545,7 +2890,7 @@ class Chat(TelegramObject):
async def send_poll(
self,
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None,
allows_multiple_answers: Optional[bool] = None,
@ -2562,6 +2907,8 @@ class Chat(TelegramObject):
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@ -2608,6 +2955,8 @@ class Chat(TelegramObject):
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
question_parse_mode=question_parse_mode,
question_entities=question_entities,
)
async def send_copy(

View file

@ -80,8 +80,9 @@ class ChatAdministratorRights(TelegramObject):
.. versionadded:: 20.6
.. versionchanged:: 21.0
|non_optional_story_argument|
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
stories posted by other users.
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
by other users, post stories to the chat page, pin chat stories, and access the chat's
story archive
.. versionadded:: 20.6
.. versionchanged:: 21.0
@ -128,8 +129,9 @@ class ChatAdministratorRights(TelegramObject):
.. versionadded:: 20.6
.. versionchanged:: 21.0
|non_optional_story_argument|
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
stories posted by other users.
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
by other users, post stories to the chat page, pin chat stories, and access the chat's
story archive
.. versionadded:: 20.6
.. versionchanged:: 21.0

540
telegram/_chatbackground.py Normal file
View file

@ -0,0 +1,540 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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 objects related to chat backgrounds."""
from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type
from telegram import constants
from telegram._files.document import Document
from telegram._telegramobject import TelegramObject
from telegram._utils import enum
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class BackgroundFill(TelegramObject):
"""Base class for Telegram BackgroundFill Objects. It can be one of:
* :class:`telegram.BackgroundFillSolid`
* :class:`telegram.BackgroundFillGradient`
* :class:`telegram.BackgroundFillFreeformGradient`
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. versionadded:: 21.2
Args:
type (:obj:`str`): Type of the background fill. Can be one of:
:attr:`~telegram.BackgroundFill.SOLID`, :attr:`~telegram.BackgroundFill.GRADIENT`
or :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`.
Attributes:
type (:obj:`str`): Type of the background fill. Can be one of:
:attr:`~telegram.BackgroundFill.SOLID`, :attr:`~telegram.BackgroundFill.GRADIENT`
or :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`.
"""
__slots__ = ("type",)
SOLID: Final[constants.BackgroundFillType] = constants.BackgroundFillType.SOLID
""":const:`telegram.constants.BackgroundFillType.SOLID`"""
GRADIENT: Final[constants.BackgroundFillType] = constants.BackgroundFillType.GRADIENT
""":const:`telegram.constants.BackgroundFillType.GRADIENT`"""
FREEFORM_GRADIENT: Final[constants.BackgroundFillType] = (
constants.BackgroundFillType.FREEFORM_GRADIENT
)
""":const:`telegram.constants.BackgroundFillType.FREEFORM_GRADIENT`"""
def __init__(
self,
type: str, # pylint: disable=redefined-builtin
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
# Required by all subclasses
self.type: str = enum.get_member(constants.BackgroundFillType, type, type)
self._id_attrs = (self.type,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BackgroundFill"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
_class_mapping: Dict[str, Type[BackgroundFill]] = {
cls.SOLID: BackgroundFillSolid,
cls.GRADIENT: BackgroundFillGradient,
cls.FREEFORM_GRADIENT: BackgroundFillFreeformGradient,
}
if cls is BackgroundFill and data.get("type") in _class_mapping:
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
return super().de_json(data=data, bot=bot)
class BackgroundFillSolid(BackgroundFill):
"""
The background is filled using the selected color.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`color` is equal.
.. versionadded:: 21.2
Args:
color (:obj:`int`): The color of the background fill in the `RGB24` format.
Attributes:
type (:obj:`str`): Type of the background fill. Always
:attr:`~telegram.BackgroundFill.SOLID`.
color (:obj:`int`): The color of the background fill in the `RGB24` format.
"""
__slots__ = ("color",)
def __init__(
self,
color: int,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(type=self.SOLID, api_kwargs=api_kwargs)
with self._unfrozen():
self.color: int = color
self._id_attrs = (self.color,)
class BackgroundFillGradient(BackgroundFill):
"""
The background is a gradient fill.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`top_color`, :attr:`bottom_color`
and :attr:`rotation_angle` are equal.
.. versionadded:: 21.2
Args:
top_color (:obj:`int`): Top color of the gradient in the `RGB24` format.
bottom_color (:obj:`int`): Bottom color of the gradient in the `RGB24` format.
rotation_angle (:obj:`int`): Clockwise rotation angle of the background
fill in degrees;
0-:tg-const:`telegram.constants.BackgroundFillLimit.MAX_ROTATION_ANGLE`.
Attributes:
type (:obj:`str`): Type of the background fill. Always
:attr:`~telegram.BackgroundFill.GRADIENT`.
top_color (:obj:`int`): Top color of the gradient in the `RGB24` format.
bottom_color (:obj:`int`): Bottom color of the gradient in the `RGB24` format.
rotation_angle (:obj:`int`): Clockwise rotation angle of the background
fill in degrees;
0-:tg-const:`telegram.constants.BackgroundFillLimit.MAX_ROTATION_ANGLE`.
"""
__slots__ = ("bottom_color", "rotation_angle", "top_color")
def __init__(
self,
top_color: int,
bottom_color: int,
rotation_angle: int,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(type=self.GRADIENT, api_kwargs=api_kwargs)
with self._unfrozen():
self.top_color: int = top_color
self.bottom_color: int = bottom_color
self.rotation_angle: int = rotation_angle
self._id_attrs = (self.top_color, self.bottom_color, self.rotation_angle)
class BackgroundFillFreeformGradient(BackgroundFill):
"""
The background is a freeform gradient that rotates after every message in the chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`colors` is equal.
.. versionadded:: 21.2
Args:
colors (Sequence[:obj:`int`]): A list of the 3 or 4 base colors that are used to
generate the freeform gradient in the `RGB24` format
Attributes:
type (:obj:`str`): Type of the background fill. Always
:attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`.
colors (Sequence[:obj:`int`]): A list of the 3 or 4 base colors that are used to
generate the freeform gradient in the `RGB24` format
"""
__slots__ = ("colors",)
def __init__(
self,
colors: Sequence[int],
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(type=self.FREEFORM_GRADIENT, api_kwargs=api_kwargs)
with self._unfrozen():
self.colors: Tuple[int, ...] = parse_sequence_arg(colors)
self._id_attrs = (self.colors,)
class BackgroundType(TelegramObject):
"""Base class for Telegram BackgroundType Objects. It can be one of:
* :class:`telegram.BackgroundTypeFill`
* :class:`telegram.BackgroundTypeWallpaper`
* :class:`telegram.BackgroundTypePattern`
* :class:`telegram.BackgroundTypeChatTheme`.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. versionadded:: 21.2
Args:
type (:obj:`str`): Type of the background. Can be one of:
:attr:`~telegram.BackgroundType.FILL`, :attr:`~telegram.BackgroundType.WALLPAPER`
:attr:`~telegram.BackgroundType.PATTERN` or
:attr:`~telegram.BackgroundType.CHAT_THEME`.
Attributes:
type (:obj:`str`): Type of the background. Can be one of:
:attr:`~telegram.BackgroundType.FILL`, :attr:`~telegram.BackgroundType.WALLPAPER`
:attr:`~telegram.BackgroundType.PATTERN` or
:attr:`~telegram.BackgroundType.CHAT_THEME`.
"""
__slots__ = ("type",)
FILL: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.FILL
""":const:`telegram.constants.BackgroundTypeType.FILL`"""
WALLPAPER: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.WALLPAPER
""":const:`telegram.constants.BackgroundTypeType.WALLPAPER`"""
PATTERN: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.PATTERN
""":const:`telegram.constants.BackgroundTypeType.PATTERN`"""
CHAT_THEME: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.CHAT_THEME
""":const:`telegram.constants.BackgroundTypeType.CHAT_THEME`"""
def __init__(
self,
type: str, # pylint: disable=redefined-builtin
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
# Required by all subclasses
self.type: str = enum.get_member(constants.BackgroundTypeType, type, type)
self._id_attrs = (self.type,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BackgroundType"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
_class_mapping: Dict[str, Type[BackgroundType]] = {
cls.FILL: BackgroundTypeFill,
cls.WALLPAPER: BackgroundTypeWallpaper,
cls.PATTERN: BackgroundTypePattern,
cls.CHAT_THEME: BackgroundTypeChatTheme,
}
if cls is BackgroundType and data.get("type") in _class_mapping:
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
if "fill" in data:
data["fill"] = BackgroundFill.de_json(data.get("fill"), bot)
if "document" in data:
data["document"] = Document.de_json(data.get("document"), bot)
return super().de_json(data=data, bot=bot)
class BackgroundTypeFill(BackgroundType):
"""
The background is automatically filled based on the selected colors.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`fill` and :attr:`dark_theme_dimming` are equal.
.. versionadded:: 21.2
Args:
fill (:obj:`telegram.BackgroundFill`): The background fill.
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
Attributes:
type (:obj:`str`): Type of the background. Always
:attr:`~telegram.BackgroundType.FILL`.
fill (:obj:`telegram.BackgroundFill`): The background fill.
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
"""
__slots__ = ("dark_theme_dimming", "fill")
def __init__(
self,
fill: BackgroundFill,
dark_theme_dimming: int,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(type=self.FILL, api_kwargs=api_kwargs)
with self._unfrozen():
self.fill: BackgroundFill = fill
self.dark_theme_dimming: int = dark_theme_dimming
self._id_attrs = (self.fill, self.dark_theme_dimming)
class BackgroundTypeWallpaper(BackgroundType):
"""
The background is a wallpaper in the `JPEG` format.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`document` and :attr:`dark_theme_dimming` are equal.
.. versionadded:: 21.2
Args:
document (:obj:`telegram.Document`): Document with the wallpaper
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
is_blurred (:obj:`bool`, optional): :obj:`True`, if the wallpaper is downscaled to fit
in a 450x450 square and then box-blurred with radius 12
is_moving (:obj:`bool`, optional): :obj:`True`, if the background moves slightly
when the device is tilted
Attributes:
type (:obj:`str`): Type of the background. Always
:attr:`~telegram.BackgroundType.WALLPAPER`.
document (:obj:`telegram.Document`): Document with the wallpaper
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
is_blurred (:obj:`bool`): Optional. :obj:`True`, if the wallpaper is downscaled to fit
in a 450x450 square and then box-blurred with radius 12
is_moving (:obj:`bool`): Optional. :obj:`True`, if the background moves slightly
when the device is tilted
"""
__slots__ = ("dark_theme_dimming", "document", "is_blurred", "is_moving")
def __init__(
self,
document: Document,
dark_theme_dimming: int,
is_blurred: Optional[bool] = None,
is_moving: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(type=self.WALLPAPER, api_kwargs=api_kwargs)
with self._unfrozen():
# Required
self.document: Document = document
self.dark_theme_dimming: int = dark_theme_dimming
# Optionals
self.is_blurred: Optional[bool] = is_blurred
self.is_moving: Optional[bool] = is_moving
self._id_attrs = (self.document, self.dark_theme_dimming)
class BackgroundTypePattern(BackgroundType):
"""
The background is a `PNG` or `TGV` (gzipped subset of `SVG` with `MIME` type
`"application/x-tgwallpattern"`) pattern to be combined with the background fill
chosen by the user.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`document` and :attr:`fill` and :attr:`intensity` are equal.
.. versionadded:: 21.2
Args:
document (:obj:`telegram.Document`): Document with the pattern.
fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with
the pattern.
intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled
background;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_INTENSITY`.
is_inverted (:obj:`int`, optional): :obj:`True`, if the background fill must be applied
only to the pattern itself. All other pixels are black in this case. For dark
themes only.
is_moving (:obj:`bool`, optional): :obj:`True`, if the background moves slightly
when the device is tilted.
Attributes:
type (:obj:`str`): Type of the background. Always
:attr:`~telegram.BackgroundType.PATTERN`.
document (:obj:`telegram.Document`): Document with the pattern.
fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with
the pattern.
intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled
background;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_INTENSITY`.
is_inverted (:obj:`int`): Optional. :obj:`True`, if the background fill must be applied
only to the pattern itself. All other pixels are black in this case. For dark
themes only.
is_moving (:obj:`bool`): Optional. :obj:`True`, if the background moves slightly
when the device is tilted.
"""
__slots__ = (
"document",
"fill",
"intensity",
"is_inverted",
"is_moving",
)
def __init__(
self,
document: Document,
fill: BackgroundFill,
intensity: int,
is_inverted: Optional[bool] = None,
is_moving: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(type=self.PATTERN, api_kwargs=api_kwargs)
with self._unfrozen():
# Required
self.document: Document = document
self.fill: BackgroundFill = fill
self.intensity: int = intensity
# Optionals
self.is_inverted: Optional[bool] = is_inverted
self.is_moving: Optional[bool] = is_moving
self._id_attrs = (self.document, self.fill, self.intensity)
class BackgroundTypeChatTheme(BackgroundType):
"""
The background is taken directly from a built-in chat theme.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`theme_name` is equal.
.. versionadded:: 21.2
Args:
theme_name (:obj:`str`): Name of the chat theme, which is usually an emoji.
Attributes:
type (:obj:`str`): Type of the background. Always
:attr:`~telegram.BackgroundType.CHAT_THEME`.
theme_name (:obj:`str`): Name of the chat theme, which is usually an emoji.
"""
__slots__ = ("theme_name",)
def __init__(
self,
theme_name: str,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(type=self.CHAT_THEME, api_kwargs=api_kwargs)
with self._unfrozen():
self.theme_name: str = theme_name
self._id_attrs = (self.theme_name,)
class ChatBackground(TelegramObject):
"""
This object represents a chat background.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal.
.. versionadded:: 21.2
Args:
type (:obj:`telegram.BackgroundType`): Type of the background.
Attributes:
type (:obj:`telegram.BackgroundType`): Type of the background.
"""
__slots__ = ("type",)
def __init__(
self,
type: BackgroundType, # pylint: disable=redefined-builtin
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.type: BackgroundType = type
self._id_attrs = (self.type,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatBackground"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["type"] = BackgroundType.de_json(data.get("type"), bot)
return super().de_json(data=data, bot=bot)

166
telegram/_chatfullinfo.py Normal file
View file

@ -0,0 +1,166 @@
#!/usr/bin/env python
# pylint: disable=redefined-builtin
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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 an object that represents a Telegram ChatFullInfo."""
from datetime import datetime
from typing import TYPE_CHECKING, Optional, Sequence
from telegram._birthdate import Birthdate
from telegram._chat import Chat
from telegram._chatlocation import ChatLocation
from telegram._chatpermissions import ChatPermissions
from telegram._files.chatphoto import ChatPhoto
from telegram._reaction import ReactionType
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import BusinessIntro, BusinessLocation, BusinessOpeningHours, Message
class ChatFullInfo(Chat):
"""
This object contains full information about a chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`~telegram.Chat.id` is equal.
Caution:
This class is a subclass of :class:`telegram.Chat` and inherits all attributes and methods
for backwards compatibility. In the future, this class will *NOT* inherit from
:class:`telegram.Chat`.
.. seealso::
All arguments and attributes can be found in :class:`telegram.Chat`.
.. versionadded:: 21.2
Args:
max_reaction_count (:obj:`int`): The maximum number of reactions that can be set on a
message in the chat.
.. versionadded:: 21.2
Attributes:
max_reaction_count (:obj:`int`): The maximum number of reactions that can be set on a
message in the chat.
.. versionadded:: 21.2
"""
__slots__ = ("max_reaction_count",)
def __init__(
self,
id: int,
type: str,
accent_color_id: int, # API 7.3 made this argument required
max_reaction_count: int, # NEW arg in api 7.3 and is required
title: Optional[str] = None,
username: Optional[str] = None,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
is_forum: Optional[bool] = None,
photo: Optional[ChatPhoto] = None,
active_usernames: Optional[Sequence[str]] = None,
birthdate: Optional[Birthdate] = None,
business_intro: Optional["BusinessIntro"] = None,
business_location: Optional["BusinessLocation"] = None,
business_opening_hours: Optional["BusinessOpeningHours"] = None,
personal_chat: Optional["Chat"] = None,
available_reactions: Optional[Sequence[ReactionType]] = None,
background_custom_emoji_id: Optional[str] = None,
profile_accent_color_id: Optional[int] = None,
profile_background_custom_emoji_id: Optional[str] = None,
emoji_status_custom_emoji_id: Optional[str] = None,
emoji_status_expiration_date: Optional[datetime] = None,
bio: Optional[str] = None,
has_private_forwards: Optional[bool] = None,
has_restricted_voice_and_video_messages: Optional[bool] = None,
join_to_send_messages: Optional[bool] = None,
join_by_request: Optional[bool] = None,
description: Optional[str] = None,
invite_link: Optional[str] = None,
pinned_message: Optional["Message"] = None,
permissions: Optional[ChatPermissions] = None,
slow_mode_delay: Optional[int] = None,
unrestrict_boost_count: Optional[int] = None,
message_auto_delete_time: Optional[int] = None,
has_aggressive_anti_spam_enabled: Optional[bool] = None,
has_hidden_members: Optional[bool] = None,
has_protected_content: Optional[bool] = None,
has_visible_history: Optional[bool] = None,
sticker_set_name: Optional[str] = None,
can_set_sticker_set: Optional[bool] = None,
custom_emoji_sticker_set_name: Optional[str] = None,
linked_chat_id: Optional[int] = None,
location: Optional[ChatLocation] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(
id=id,
type=type,
title=title,
username=username,
first_name=first_name,
last_name=last_name,
photo=photo,
description=description,
invite_link=invite_link,
pinned_message=pinned_message,
permissions=permissions,
sticker_set_name=sticker_set_name,
can_set_sticker_set=can_set_sticker_set,
slow_mode_delay=slow_mode_delay,
bio=bio,
linked_chat_id=linked_chat_id,
location=location,
message_auto_delete_time=message_auto_delete_time,
has_private_forwards=has_private_forwards,
has_protected_content=has_protected_content,
join_to_send_messages=join_to_send_messages,
join_by_request=join_by_request,
has_restricted_voice_and_video_messages=has_restricted_voice_and_video_messages,
is_forum=is_forum,
active_usernames=active_usernames,
emoji_status_custom_emoji_id=emoji_status_custom_emoji_id,
emoji_status_expiration_date=emoji_status_expiration_date,
has_aggressive_anti_spam_enabled=has_aggressive_anti_spam_enabled,
has_hidden_members=has_hidden_members,
available_reactions=available_reactions,
accent_color_id=accent_color_id,
background_custom_emoji_id=background_custom_emoji_id,
profile_accent_color_id=profile_accent_color_id,
profile_background_custom_emoji_id=profile_background_custom_emoji_id,
has_visible_history=has_visible_history,
unrestrict_boost_count=unrestrict_boost_count,
custom_emoji_sticker_set_name=custom_emoji_sticker_set_name,
birthdate=birthdate,
personal_chat=personal_chat,
business_intro=business_intro,
business_location=business_location,
business_opening_hours=business_opening_hours,
api_kwargs=api_kwargs,
)
# Required and unique to this class-
with self._unfrozen():
self.max_reaction_count: int = max_reaction_count
self._freeze()

View file

@ -235,8 +235,9 @@ class ChatMemberAdministrator(ChatMember):
.. versionadded:: 20.6
.. versionchanged:: 21.0
|non_optional_story_argument|
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
stories posted by other users.
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
by other users, post stories to the chat page, pin chat stories, and access the chat's
story archive
.. versionadded:: 20.6
.. versionchanged:: 21.0
@ -294,8 +295,9 @@ class ChatMemberAdministrator(ChatMember):
.. versionadded:: 20.6
.. versionchanged:: 21.0
|non_optional_story_argument|
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
stories posted by other users.
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
by other users, post stories to the chat page, pin chat stories, and access the chat's
story archive
.. versionadded:: 20.6
.. versionchanged:: 21.0

View file

@ -63,6 +63,11 @@ class ChatMemberUpdated(TelegramObject):
chat via a chat folder invite link
.. versionadded:: 20.3
via_join_request (:obj:`bool`, optional): :obj:`True`, if the user joined the chat after
sending a direct join request without using an invite link and being approved by
an administrator
.. versionadded:: 21.2
Attributes:
chat (:class:`telegram.Chat`): Chat the user belongs to.
@ -80,6 +85,11 @@ class ChatMemberUpdated(TelegramObject):
chat via a chat folder invite link
.. versionadded:: 20.3
via_join_request (:obj:`bool`): Optional. :obj:`True`, if the user joined the chat after
sending a direct join request without using an invite link and being approved
by an administrator
.. versionadded:: 21.2
"""
@ -91,6 +101,7 @@ class ChatMemberUpdated(TelegramObject):
"new_chat_member",
"old_chat_member",
"via_chat_folder_invite_link",
"via_join_request",
)
def __init__(
@ -102,6 +113,7 @@ class ChatMemberUpdated(TelegramObject):
new_chat_member: ChatMember,
invite_link: Optional[ChatInviteLink] = None,
via_chat_folder_invite_link: Optional[bool] = None,
via_join_request: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@ -116,6 +128,7 @@ class ChatMemberUpdated(TelegramObject):
# Optionals
self.invite_link: Optional[ChatInviteLink] = invite_link
self.via_join_request: Optional[bool] = via_join_request
self._id_attrs = (
self.chat,

View file

@ -235,7 +235,7 @@ class StickerSet(TelegramObject):
.. versionchanged:: 20.5
|removed_thumb_note|
.. versionremoved:: NEXT.VERSION
.. versionremoved:: 21.2
Removed the deprecated arguments and attributes ``is_animated`` and ``is_video``.
Args:

View file

@ -30,7 +30,8 @@ class ForceReply(TelegramObject):
Upon receiving a message with this object, Telegram clients will display a reply interface to
the user (act as if the user has selected the bot's message and tapped 'Reply'). This can be
extremely useful if you want to create user-friendly step-by-step interfaces without having
to sacrifice privacy mode.
to sacrifice `privacy mode <https://core.telegram.org/bots/features#privacy-mode>`_. Not
supported in channels and for messages sent on behalf of a Telegram Business account.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`selective` is equal.

View file

@ -91,6 +91,7 @@ class InlineKeyboardButton(TelegramObject):
to the bot when button is pressed, UTF-8
:tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`-
:tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes.
Not supported for messages sent on behalf of a Telegram Business account.
If the bot instance allows arbitrary callback data, anything can be passed.
Tip:
@ -102,25 +103,25 @@ class InlineKeyboardButton(TelegramObject):
<https://core.telegram.org/bots/webapps>`_ that will be launched when the user presses
the button. The Web App will be able to send an arbitrary message on behalf of the user
using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in
private chats between a user and the bot.
private chats between a user and the bot. Not supported for messages sent on behalf of
a Telegram Business account.
.. versionadded:: 20.0
switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the
user to select one of their chats, open that chat and insert the bot's username and the
specified inline query in the input field. Can be empty, in which case just the bot's
username will be inserted. This offers an easy way for users to start using your bot
in inline mode when they are currently in a private chat with it. Especially useful
when combined with ``switch_pm*`` actions - in this case the user will be automatically
returned to the chat they switched from, skipping the chat selection screen.
switch_inline_query (:obj:`str`, optional): If set, pressing the button will insert the
bot's username and the specified inline query in the current chat's input field. May be
empty, in which case only the bot's username will be inserted.
This offers a quick way for the user to open your bot in inline mode in the same chat -
good for selecting something from multiple options. Not supported in channels and for
messages sent on behalf of a Telegram Business account.
Tip:
This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
but gives no control over which chats can be selected.
switch_inline_query_current_chat (:obj:`str`, optional): If set, pressing the button will
insert the bot's username and the specified inline query in the current chat's input
field. Can be empty, in which case only the bot's username will be inserted. This
offers a quick way for the user to open your bot in inline mode in the same chat - good
for selecting something from multiple options.
prompt the user to select one of their chats of the specified type, open that chat and
insert the bot's username and the specified inline query in the input field. Not
supported for messages sent on behalf of a Telegram Business account.
callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will
be launched when the user presses the button. This type of button **must** always be
the **first** button in the first row.
@ -130,7 +131,8 @@ class InlineKeyboardButton(TelegramObject):
switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`, optional):
If set, pressing the button will prompt the user to select one of their chats of the
specified type, open that chat and insert the bot's username and the specified inline
query in the input field.
query in the input field. Not supported for messages sent on behalf of a Telegram
Business account.
.. versionadded:: 20.3
@ -159,29 +161,30 @@ class InlineKeyboardButton(TelegramObject):
to the bot when button is pressed, UTF-8
:tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`-
:tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes.
Not supported for messages sent on behalf of a Telegram Business account.
web_app (:obj:`telegram.WebAppInfo`): Optional. Description of the `Web App
<https://core.telegram.org/bots/webapps>`_ that will be launched when the user presses
the button. The Web App will be able to send an arbitrary message on behalf of the user
using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in
private chats between a user and the bot.
private chats between a user and the bot. Not supported for messages sent on behalf of
a Telegram Business account.
.. versionadded:: 20.0
switch_inline_query (:obj:`str`): Optional. If set, pressing the button will prompt the
user to select one of their chats, open that chat and insert the bot's username and the
specified inline query in the input field. Can be empty, in which case just the bot's
username will be inserted. This offers an easy way for users to start using your bot
in inline mode when they are currently in a private chat with it. Especially useful
when combined with ``switch_pm*`` actions - in this case the user will be automatically
returned to the chat they switched from, skipping the chat selection screen.
switch_inline_query (:obj:`str`): Optional. If set, pressing the button will insert the
bot's username and the specified inline query in the current chat's input field. May be
empty, in which case only the bot's username will be inserted.
This offers a quick way for the user to open your bot in inline mode in the same chat -
good for selecting something from multiple options. Not supported in channels and for
messages sent on behalf of a Telegram Business account.
Tip:
This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
but gives no control over which chats can be selected.
switch_inline_query_current_chat (:obj:`str`): Optional. If set, pressing the button will
insert the bot's username and the specified inline query in the current chat's input
field. Can be empty, in which case only the bot's username will be inserted. This
offers a quick way for the user to open your bot in inline mode in the same chat - good
for selecting something from multiple options.
prompt the user to select one of their chats of the specified type, open that chat and
insert the bot's username and the specified inline query in the input field. Not
supported for messages sent on behalf of a Telegram Business account.
callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will
be launched when the user presses the button. This type of button **must** always be
the **first** button in the first row.
@ -191,7 +194,8 @@ class InlineKeyboardButton(TelegramObject):
switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`): Optional.
If set, pressing the button will prompt the user to select one of their chats of the
specified type, open that chat and insert the bot's username and the specified inline
query in the input field.
query in the input field. Not supported for messages sent on behalf of a Telegram
Business account.
.. versionadded:: 20.3

View file

@ -89,7 +89,9 @@ class InlineQueryResultLocation(InlineQueryResult):
live_period (:obj:`int`): Optional. Period in seconds for which the location will be
updated, should be between
:tg-const:`telegram.InlineQueryResultLocation.MIN_LIVE_PERIOD` and
:tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD`.
:tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD` or
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
locations that can be edited indefinitely.
heading (:obj:`int`): Optional. For live locations, a direction in which the user is
moving, in degrees. Must be between
:tg-const:`telegram.InlineQueryResultLocation.MIN_HEADING` and

View file

@ -42,7 +42,9 @@ class InputLocationMessageContent(InputMessageContent):
live_period (:obj:`int`, optional): Period in seconds for which the location will be
updated, should be between
:tg-const:`telegram.InputLocationMessageContent.MIN_LIVE_PERIOD` and
:tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD`.
:tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD` or
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
locations that can be edited indefinitely.
heading (:obj:`int`, optional): For live locations, a direction in which the user is
moving, in degrees. Must be between
:tg-const:`telegram.InputLocationMessageContent.MIN_HEADING` and

View file

@ -25,6 +25,7 @@ from html import escape
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, TypedDict, Union
from telegram._chat import Chat
from telegram._chatbackground import ChatBackground
from telegram._chatboost import ChatBoostAdded
from telegram._dice import Dice
from telegram._files.animation import Animation
@ -64,6 +65,7 @@ from telegram._user import User
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
from telegram._utils.entities import parse_message_entities, parse_message_entity
from telegram._utils.types import (
CorrectOptionID,
FileInput,
@ -99,6 +101,7 @@ if TYPE_CHECKING:
InputMediaDocument,
InputMediaPhoto,
InputMediaVideo,
InputPollOption,
LabeledPrice,
MessageId,
MessageOrigin,
@ -553,6 +556,11 @@ class Message(MaybeInaccessibleMessage):
.. versionadded:: 21.1
chat_background_set (:obj:`telegram.ChatBackground`, optional): Service message: chat
background set.
.. versionadded:: 21.2
Attributes:
message_id (:obj:`int`): Unique message identifier inside this chat.
from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages
@ -853,6 +861,11 @@ class Message(MaybeInaccessibleMessage):
.. versionadded:: 21.1
chat_background_set (:obj:`telegram.ChatBackground`): Optional. Service message: chat
background set
.. versionadded:: 21.2
.. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by
:attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a
:exc:`ValueError` when encountering a custom emoji.
@ -876,6 +889,7 @@ class Message(MaybeInaccessibleMessage):
"caption",
"caption_entities",
"channel_chat_created",
"chat_background_set",
"chat_shared",
"connected_website",
"contact",
@ -1029,6 +1043,7 @@ class Message(MaybeInaccessibleMessage):
business_connection_id: Optional[str] = None,
sender_business_bot: Optional[User] = None,
is_from_offline: Optional[bool] = None,
chat_background_set: Optional[ChatBackground] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@ -1127,6 +1142,7 @@ class Message(MaybeInaccessibleMessage):
self.business_connection_id: Optional[str] = business_connection_id
self.sender_business_bot: Optional[User] = sender_business_bot
self.is_from_offline: Optional[bool] = is_from_offline
self.chat_background_set: Optional[ChatBackground] = chat_background_set
self._effective_attachment = DEFAULT_NONE
@ -1241,6 +1257,7 @@ class Message(MaybeInaccessibleMessage):
)
data["users_shared"] = UsersShared.de_json(data.get("users_shared"), bot)
data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot)
data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot)
# Unfortunately, this needs to be here due to cyclic imports
from telegram._giveaway import ( # pylint: disable=import-outside-toplevel
@ -1562,9 +1579,11 @@ class Message(MaybeInaccessibleMessage):
if quote is not None:
warn(
"The `quote` parameter is deprecated in favor of the `do_quote` parameter. Please "
"update your code to use `do_quote` instead.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.8",
"The `quote` parameter is deprecated in favor of the `do_quote` parameter. "
"Please update your code to use `do_quote` instead.",
),
stacklevel=2,
)
@ -2890,7 +2909,7 @@ class Message(MaybeInaccessibleMessage):
async def reply_poll(
self,
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None, # pylint: disable=redefined-builtin
allows_multiple_answers: Optional[bool] = None,
@ -2906,6 +2925,8 @@ class Message(MaybeInaccessibleMessage):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: ODVInput[int] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@ -2976,6 +2997,8 @@ class Message(MaybeInaccessibleMessage):
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=self.business_connection_id,
question_parse_mode=question_parse_mode,
question_entities=question_entities,
)
async def reply_dice(
@ -3653,6 +3676,7 @@ class Message(MaybeInaccessibleMessage):
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
live_period: Optional[int] = None,
*,
location: Optional[Location] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -3694,6 +3718,7 @@ class Message(MaybeInaccessibleMessage):
horizontal_accuracy=horizontal_accuracy,
heading=heading,
proximity_alert_radius=proximity_alert_radius,
live_period=live_period,
inline_message_id=None,
)
@ -4184,9 +4209,7 @@ class Message(MaybeInaccessibleMessage):
if not self.text:
raise RuntimeError("This Message has no 'text'.")
entity_text = self.text.encode("utf-16-le")
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode("utf-16-le")
return parse_message_entity(self.text, entity)
def parse_caption_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
@ -4210,9 +4233,7 @@ class Message(MaybeInaccessibleMessage):
if not self.caption:
raise RuntimeError("This Message has no 'caption'.")
entity_text = self.caption.encode("utf-16-le")
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode("utf-16-le")
return parse_message_entity(self.caption, entity)
def parse_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]:
"""
@ -4237,12 +4258,7 @@ class Message(MaybeInaccessibleMessage):
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
if types is None:
types = MessageEntity.ALL_TYPES
return {
entity: self.parse_entity(entity) for entity in self.entities if entity.type in types
}
return parse_message_entities(self.text, self.entities, types=types)
def parse_caption_entities(
self, types: Optional[List[str]] = None
@ -4269,14 +4285,7 @@ class Message(MaybeInaccessibleMessage):
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
if types is None:
types = MessageEntity.ALL_TYPES
return {
entity: self.parse_caption_entity(entity)
for entity in self.caption_entities
if entity.type in types
}
return parse_message_entities(self.caption, self.caption_entities, types=types)
@classmethod
def _parse_html(

View file

@ -210,9 +210,11 @@ class PassportElementErrorFiles(PassportElementError):
This attribute will return a tuple instead of a list in future major versions.
"""
warn(
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.6",
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions.",
),
stacklevel=2,
)
return self._file_hashes
@ -427,10 +429,12 @@ class PassportElementErrorTranslationFiles(PassportElementError):
This attribute will return a tuple instead of a list in future major versions.
"""
warn(
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions. See the stability policy:"
" https://docs.python-telegram-bot.org/en/stable/stability_policy.html",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.6",
"The attribute `file_hashes` will return a tuple instead of a list in future major"
" versions. See the stability policy:"
" https://docs.python-telegram-bot.org/en/stable/stability_policy.html",
),
stacklevel=2,
)
return self._file_hashes

View file

@ -107,9 +107,11 @@ class PassportFile(TelegramObject):
This attribute will return a datetime instead of a integer in future major versions.
"""
warn(
"The attribute `file_date` will return a datetime instead of an integer in future"
" major versions.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.6",
"The attribute `file_date` will return a datetime instead of an integer in future"
" major versions.",
),
stacklevel=2,
)
return self._file_date

View file

@ -28,12 +28,80 @@ from telegram._user import User
from telegram._utils import enum
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.entities import parse_message_entities, parse_message_entity
from telegram._utils.types import JSONDict, ODVInput
if TYPE_CHECKING:
from telegram import Bot
class InputPollOption(TelegramObject):
"""
This object contains information about one answer option in a poll to send.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`text` is equal.
.. versionadded:: 21.2
Args:
text (:obj:`str`): Option text,
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
text_parse_mode (:obj:`str`, optional): |parse_mode|
Currently, only custom emoji entities are allowed.
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
that appear in the option :paramref:`text`. It can be specified instead of
:paramref:`text_parse_mode`.
Currently, only custom emoji entities are allowed.
This list is empty if the text does not contain entities.
Attributes:
text (:obj:`str`): Option text,
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
text_parse_mode (:obj:`str`): Optional. |parse_mode|
Currently, only custom emoji entities are allowed.
text_entities (Sequence[:class:`telegram.MessageEntity`]): Special entities
that appear in the option :paramref:`text`. It can be specified instead of
:paramref:`text_parse_mode`.
Currently, only custom emoji entities are allowed.
This list is empty if the text does not contain entities.
"""
__slots__ = ("text", "text_entities", "text_parse_mode")
def __init__(
self,
text: str,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.text: str = text
self.text_parse_mode: ODVInput[str] = text_parse_mode
self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
self._id_attrs = (self.text,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InputPollOption"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
return super().de_json(data=data, bot=bot)
class PollOption(TelegramObject):
"""
This object contains information about one answer option in a poll.
@ -46,26 +114,101 @@ class PollOption(TelegramObject):
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
voter_count (:obj:`int`): Number of users that voted for this option.
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
that appear in the option text. Currently, only custom emoji entities are allowed in
poll option texts.
.. versionadded:: 21.2
Attributes:
text (:obj:`str`): Option text,
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
voter_count (:obj:`int`): Number of users that voted for this option.
text_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities
that appear in the option text. Currently, only custom emoji entities are allowed in
poll option texts.
This list is empty if the question does not contain entities.
.. versionadded:: 21.2
"""
__slots__ = ("text", "voter_count")
__slots__ = ("text", "text_entities", "voter_count")
def __init__(self, text: str, voter_count: int, *, api_kwargs: Optional[JSONDict] = None):
def __init__(
self,
text: str,
voter_count: int,
text_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.text: str = text
self.voter_count: int = voter_count
self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
self._id_attrs = (self.text, self.voter_count)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PollOption"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
return super().de_json(data=data, bot=bot)
def parse_entity(self, entity: MessageEntity) -> str:
"""Returns the text in :attr:`text`
from a given :class:`telegram.MessageEntity` of :attr:`text_entities`.
Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice ``Message.text`` with the offset and length.)
.. versionadded:: 21.2
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to :attr:`text_entities`.
Returns:
:obj:`str`: The text of the given entity.
"""
return parse_message_entity(self.text, entity)
def parse_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this polls question filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
Note:
This method should always be used instead of the :attr:`text_entities`
attribute, since it calculates the correct substring from the message text based on
UTF-16 codepoints. See :attr:`parse_entity` for more info.
.. versionadded:: 21.2
Args:
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
Returns:
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
return parse_message_entities(self.text, self.text_entities, types)
MIN_LENGTH: Final[int] = constants.PollLimit.MIN_OPTION_LENGTH
""":const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH`
@ -215,6 +358,11 @@ class Poll(TelegramObject):
.. versionchanged:: 20.3
|datetime_localization|
question_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
that appear in the :attr:`question`. Currently, only custom emoji entities are allowed
in poll questions.
.. versionadded:: 21.2
Attributes:
id (:obj:`str`): Unique poll identifier.
@ -251,6 +399,12 @@ class Poll(TelegramObject):
.. versionchanged:: 20.3
|datetime_localization|
question_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities
that appear in the :attr:`question`. Currently, only custom emoji entities are allowed
in poll questions.
This list is empty if the question does not contain entities.
.. versionadded:: 21.2
"""
@ -266,6 +420,7 @@ class Poll(TelegramObject):
"open_period",
"options",
"question",
"question_entities",
"total_voter_count",
"type",
)
@ -285,6 +440,7 @@ class Poll(TelegramObject):
explanation_entities: Optional[Sequence[MessageEntity]] = None,
open_period: Optional[int] = None,
close_date: Optional[datetime.datetime] = None,
question_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@ -304,6 +460,7 @@ class Poll(TelegramObject):
)
self.open_period: Optional[int] = open_period
self.close_date: Optional[datetime.datetime] = close_date
self.question_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(question_entities)
self._id_attrs = (self.id,)
@ -323,11 +480,13 @@ class Poll(TelegramObject):
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"), tzinfo=loc_tzinfo)
data["question_entities"] = MessageEntity.de_list(data.get("question_entities"), bot)
return super().de_json(data=data, bot=bot)
def parse_explanation_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
"""Returns the text in :attr:`explanation` from a given :class:`telegram.MessageEntity` of
:attr:`explanation_entities`.
Note:
This method is present because Telegram calculates the offset and length in
@ -336,7 +495,7 @@ class Poll(TelegramObject):
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to this message.
be an entity that belongs to :attr:`explanation_entities`.
Returns:
:obj:`str`: The text of the given entity.
@ -348,10 +507,7 @@ class Poll(TelegramObject):
if not self.explanation:
raise RuntimeError("This Poll has no 'explanation'.")
entity_text = self.explanation.encode("utf-16-le")
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode("utf-16-le")
return parse_message_entity(self.explanation, entity)
def parse_explanation_entities(
self, types: Optional[List[str]] = None
@ -375,15 +531,61 @@ class Poll(TelegramObject):
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
if types is None:
types = MessageEntity.ALL_TYPES
Raises:
RuntimeError: If the poll has no explanation.
return {
entity: self.parse_explanation_entity(entity)
for entity in self.explanation_entities
if entity.type in types
}
"""
if not self.explanation:
raise RuntimeError("This Poll has no 'explanation'.")
return parse_message_entities(self.explanation, self.explanation_entities, types)
def parse_question_entity(self, entity: MessageEntity) -> str:
"""Returns the text in :attr:`question` from a given :class:`telegram.MessageEntity` of
:attr:`question_entities`.
.. versionadded:: 21.2
Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice ``Message.text`` with the offset and length.)
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to :attr:`question_entities`.
Returns:
:obj:`str`: The text of the given entity.
"""
return parse_message_entity(self.question, entity)
def parse_question_entities(
self, types: Optional[List[str]] = None
) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this polls question filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
.. versionadded:: 21.2
Note:
This method should always be used instead of the :attr:`question_entities`
attribute, since it calculates the correct substring from the message text based on
UTF-16 codepoints. See :attr:`parse_question_entity` for more info.
Args:
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
Returns:
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
return parse_message_entities(self.question, self.question_entities, types)
REGULAR: Final[str] = constants.PollType.REGULAR
""":const:`telegram.constants.PollType.REGULAR`"""

View file

@ -355,6 +355,7 @@ class ReplyParameters(TelegramObject):
chat, or in the chat :paramref:`chat_id` if it is specified.
chat_id (:obj:`int` | :obj:`str`, optional): If the message to be replied to is from a
different chat, |chat_id_channel|
Not supported for messages sent on behalf of a business account.
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| Can be
used only for replies in the same chat and forum topic.
quote (:obj:`str`, optional): Quoted part of the message to be replied to; 0-1024
@ -376,6 +377,7 @@ class ReplyParameters(TelegramObject):
chat, or in the chat :paramref:`chat_id` if it is specified.
chat_id (:obj:`int` | :obj:`str`): Optional. If the message to be replied to is from a
different chat, |chat_id_channel|
Not supported for messages sent on behalf of a business account.
allow_sending_without_reply (:obj:`bool`): Optional. |allow_sending_without_reply| Can be
used only for replies in the same chat and forum topic.
quote (:obj:`str`): Optional. Quoted part of the message to be replied to; 0-1024

View file

@ -28,7 +28,8 @@ from telegram._utils.types import JSONDict
class ReplyKeyboardMarkup(TelegramObject):
"""This object represents a custom keyboard with reply options.
"""This object represents a custom keyboard with reply options. Not supported in channels and
for messages sent on behalf of a Telegram Business account.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their size of :attr:`keyboard` and all the buttons are equal.

View file

@ -29,6 +29,7 @@ class ReplyKeyboardRemove(TelegramObject):
keyboard and display the default letter-keyboard. By default, custom keyboards are displayed
until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are
hidden immediately after the user presses a button (see :class:`telegram.ReplyKeyboardMarkup`).
Not supported in channels and for messages sent on behalf of a Telegram Business account.
Note:
User will not be able to summon this keyboard; if you want to hide the keyboard from

View file

@ -44,7 +44,7 @@ class UsersShared(TelegramObject):
The argument :attr:`users` is now considered for the equality comparison instead of
``user_ids``.
.. versionremoved:: NEXT.VERSION
.. versionremoved:: 21.2
Removed the deprecated argument and attribute ``user_ids``.
Args:
@ -54,7 +54,7 @@ class UsersShared(TelegramObject):
.. versionadded:: 21.1
.. versionchanged:: NEXT.VERSION
.. versionchanged:: 21.2
This argument is now required.
Attributes:

View file

@ -141,8 +141,8 @@ class Update(TelegramObject):
.. versionadded:: 21.1
business_message (:class:`telegram.Message`, optional): New non-service message
from a connected business account.
business_message (:class:`telegram.Message`, optional): New message from a connected
business account.
.. versionadded:: 21.1
@ -249,8 +249,8 @@ class Update(TelegramObject):
.. versionadded:: 21.1
business_message (:class:`telegram.Message`): Optional. New non-service message
from a connected business account.
business_message (:class:`telegram.Message`): Optional. New message from a connected
business account.
.. versionadded:: 21.1

View file

@ -40,6 +40,7 @@ if TYPE_CHECKING:
InputMediaDocument,
InputMediaPhoto,
InputMediaVideo,
InputPollOption,
LabeledPrice,
LinkPreviewOptions,
Location,
@ -1482,7 +1483,7 @@ class User(TelegramObject):
async def send_poll(
self,
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None,
allows_multiple_answers: Optional[bool] = None,
@ -1499,6 +1500,8 @@ class User(TelegramObject):
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@ -1548,6 +1551,8 @@ class User(TelegramObject):
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
question_parse_mode=question_parse_mode,
question_entities=question_entities,
)
async def send_copy(

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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 auxiliary functionality for parsing MessageEntity objects.
Warning:
Contents of this module are intended to be used internally by the library and *not* by the
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""
from typing import Dict, Optional, Sequence
from telegram._messageentity import MessageEntity
def parse_message_entity(text: str, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
Args:
text (:obj:`str`): The text to extract the entity from.
entity (:class:`telegram.MessageEntity`): The entity to extract the text from.
Returns:
:obj:`str`: The text of the given entity.
"""
entity_text = text.encode("utf-16-le")
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode("utf-16-le")
def parse_message_entities(
text: str, entities: Sequence[MessageEntity], types: Optional[Sequence[str]] = None
) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
Args:
text (:obj:`str`): The text to extract the entity from.
entities (List[:class:`telegram.MessageEntity`]): The entities to extract the text from.
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
Returns:
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
if types is None:
types = MessageEntity.ALL_TYPES
return {
entity: parse_message_entity(text, entity) for entity in entities if entity.type in types
}

View file

@ -26,19 +26,28 @@ Warning:
the changelog.
"""
import warnings
from typing import Type
from typing import Type, Union
from telegram.warnings import PTBUserWarning
def warn(message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0) -> None:
def warn(
message: Union[str, PTBUserWarning],
category: Type[Warning] = PTBUserWarning,
stacklevel: int = 0,
) -> None:
"""
Helper function used as a shortcut for warning with default values.
.. versionadded:: 20.0
Args:
message (:obj:`str`): Specify the warnings message to pass to ``warnings.warn()``.
message (:obj:`str` | :obj:`PTBUserWarning`): Specify the warnings message to pass to
``warnings.warn()``.
.. versionchanged:: 21.2
Now also accepts a :obj:`PTBUserWarning` instance.
category (:obj:`Type[Warning]`, optional): Specify the Warning class to pass to
``warnings.warn()``. Defaults to :class:`telegram.warnings.PTBUserWarning`.
stacklevel (:obj:`int`, optional): Specify the stacklevel to pass to ``warnings.warn()``.

View file

@ -23,10 +23,10 @@ inside warnings.py.
.. versionadded:: 20.2
"""
from typing import Any, Callable, Type
from typing import Any, Callable, Type, Union
from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
def build_deprecation_warning_message(
@ -54,8 +54,9 @@ def warn_about_deprecated_arg_return_new_arg(
deprecated_arg_name: str,
new_arg_name: str,
bot_api_version: str,
ptb_version: str,
stacklevel: int = 2,
warn_callback: Callable[[str, Type[Warning], int], None] = warn,
warn_callback: Callable[[Union[str, PTBUserWarning], Type[Warning], int], None] = warn,
) -> Any:
"""A helper function for the transition in API when argument is renamed.
@ -80,10 +81,12 @@ def warn_about_deprecated_arg_return_new_arg(
if deprecated_arg:
warn_callback(
f"Bot API {bot_api_version} renamed the argument '{deprecated_arg_name}' to "
f"'{new_arg_name}'.",
PTBDeprecationWarning,
stacklevel + 1,
PTBDeprecationWarning(
ptb_version,
f"Bot API {bot_api_version} renamed the argument '{deprecated_arg_name}' to "
f"'{new_arg_name}'.",
),
stacklevel=stacklevel + 1, # type: ignore[call-arg]
)
return deprecated_arg
@ -94,6 +97,7 @@ def warn_about_deprecated_attr_in_property(
deprecated_attr_name: str,
new_attr_name: str,
bot_api_version: str,
ptb_version: str,
stacklevel: int = 2,
) -> None:
"""A helper function for the transition in API when attribute is renamed. Call from properties.
@ -101,8 +105,10 @@ def warn_about_deprecated_attr_in_property(
The properties replace deprecated attributes in classes and issue these deprecation warnings.
"""
warn(
f"Bot API {bot_api_version} renamed the attribute '{deprecated_attr_name}' to "
f"'{new_attr_name}'.",
PTBDeprecationWarning,
PTBDeprecationWarning(
ptb_version,
f"Bot API {bot_api_version} renamed the attribute '{deprecated_attr_name}' to "
f"'{new_attr_name}'.",
),
stacklevel=stacklevel + 1,
)

View file

@ -51,7 +51,7 @@ class Version(NamedTuple):
__version_info__: Final[Version] = Version(
major=21, minor=1, micro=1, releaselevel="final", serial=0
major=21, minor=2, micro=0, releaselevel="final", serial=0
)
__version__: Final[str] = str(__version_info__)

View file

@ -37,6 +37,10 @@ __all__ = [
"SUPPORTED_WEBHOOK_PORTS",
"ZERO_DATE",
"AccentColor",
"BackgroundFillLimit",
"BackgroundFillType",
"BackgroundTypeLimit",
"BackgroundTypeType",
"BotCommandLimit",
"BotCommandScopeType",
"BotDescriptionLimit",
@ -142,7 +146,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=2)
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=3)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@ -822,6 +826,46 @@ class ChatLimit(IntEnum):
"""
class BackgroundTypeLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.BackgroundTypeFill`,
:class:`telegram.BackgroundTypeWallpaper` and :class:`telegram.BackgroundTypePattern`.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
MAX_DIMMING = 100
""":obj:`int`: Maximum value allowed for:
* :paramref:`~telegram.BackgroundTypeFill.dark_theme_dimming` parameter of
:class:`telegram.BackgroundTypeFill`
* :paramref:`~telegram.BackgroundTypeWallpaper.dark_theme_dimming` parameter of
:class:`telegram.BackgroundTypeWallpaper`
"""
MAX_INTENSITY = 100
""":obj:`int`: Maximum value allowed for :paramref:`~telegram.BackgroundTypePattern.intensity`
parameter of :class:`telegram.BackgroundTypePattern`
"""
class BackgroundFillLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.BackgroundFillGradient`.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
MAX_ROTATION_ANGLE = 359
""":obj:`int`: Maximum value allowed for:
:paramref:`~telegram.BackgroundFillGradient.rotation_angle` parameter of
:class:`telegram.BackgroundFillGradient`
"""
class ChatMemberStatus(StringEnum):
"""This enum contains the available states for :class:`telegram.ChatMember`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
@ -1427,6 +1471,21 @@ class LocationLimit(IntEnum):
:meth:`telegram.Bot.send_location`
"""
LIVE_PERIOD_FOREVER = int(hex(0x7FFFFFFF), 16)
""":obj:`int`: Value for live locations that can be edited indefinitely. Passed in:
* :paramref:`~telegram.InlineQueryResultLocation.live_period` parameter of
:class:`telegram.InlineQueryResultLocation`
* :paramref:`~telegram.InputLocationMessageContent.live_period` parameter of
:class:`telegram.InputLocationMessageContent`
* :paramref:`~telegram.Bot.edit_message_live_location.live_period` parameter of
:meth:`telegram.Bot.edit_message_live_location`
* :paramref:`~telegram.Bot.send_location.live_period` parameter of
:meth:`telegram.Bot.send_location`
.. versionadded:: 21.2
"""
MIN_PROXIMITY_ALERT_RADIUS = 1
""":obj:`int`: Minimum value allowed for:
@ -1726,6 +1785,11 @@ class MessageType(StringEnum):
.. versionadded:: 20.8
"""
CHAT_BACKGROUND_SET = "chat_background_set"
""":obj:`str`: Messages with :attr:`telegram.Message.chat_background_set`.
.. versionadded:: 21.2
"""
CONNECTED_WEBSITE = "connected_website"
""":obj:`str`: Messages with :attr:`telegram.Message.connected_website`."""
CONTACT = "contact"
@ -2878,3 +2942,39 @@ class ReactionEmoji(StringEnum):
""":obj:`str`: Woman Shrugging"""
POUTING_FACE = "😡"
""":obj:`str`: Pouting face"""
class BackgroundTypeType(StringEnum):
"""This enum contains the available types of :class:`telegram.BackgroundType`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
FILL = "fill"
""":obj:`str`: A :class:`telegram.BackgroundType` with fill background."""
WALLPAPER = "wallpaper"
""":obj:`str`: A :class:`telegram.BackgroundType` with wallpaper background."""
PATTERN = "pattern"
""":obj:`str`: A :class:`telegram.BackgroundType` with pattern background."""
CHAT_THEME = "chat_theme"
""":obj:`str`: A :class:`telegram.BackgroundType` with chat_theme background."""
class BackgroundFillType(StringEnum):
"""This enum contains the available types of :class:`telegram.BackgroundFill`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
SOLID = "solid"
""":obj:`str`: A :class:`telegram.BackgroundFill` with solid fill."""
GRADIENT = "gradient"
""":obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill."""
FREEFORM_GRADIENT = "freeform_gradient"
""":obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill."""

View file

@ -365,6 +365,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
self.__update_persistence_event = asyncio.Event()
self.__update_persistence_lock = asyncio.Lock()
self.__create_task_tasks: Set[asyncio.Task] = set() # Used for awaiting tasks upon exit
self.__stop_running_marker = asyncio.Event()
async def __aenter__(self: _AppType) -> _AppType: # noqa: PYI019
"""|async_context_manager| :meth:`initializes <initialize>` the App.
@ -516,6 +517,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
await self._add_ch_to_persistence(handler)
self._initialized = True
self.__stop_running_marker.clear()
async def _add_ch_to_persistence(self, handler: "ConversationHandler") -> None:
self._conversation_handler_conversations.update(
@ -670,14 +672,26 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
raise RuntimeError("This Application is not running!")
self._running = False
self.__stop_running_marker.clear()
_LOGGER.info("Application is stopping. This might take a moment.")
# Stop listening for new updates and handle all pending ones
await self.update_queue.put(_STOP_SIGNAL)
_LOGGER.debug("Waiting for update_queue to join")
await self.update_queue.join()
if self.__update_fetcher_task:
await self.__update_fetcher_task
if self.__update_fetcher_task.done():
try:
self.__update_fetcher_task.result()
except BaseException as exc:
_LOGGER.critical(
"Fetching updates was aborted due to %r. Suppressing "
"exception to ensure graceful shutdown.",
exc,
exc_info=True,
)
else:
await self.update_queue.put(_STOP_SIGNAL)
_LOGGER.debug("Waiting for update_queue to join")
await self.update_queue.join()
await self.__update_fetcher_task
_LOGGER.debug("Application stopped fetching of updates.")
if self._job_queue:
@ -703,17 +717,36 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
shutdown of the application, i.e. the methods listed in :attr:`run_polling` and
:attr:`run_webhook` will still be executed.
This method can also be called within :meth:`post_init`. This allows for a graceful,
early shutdown of the application if some condition is met (e.g., a database connection
could not be established).
Note:
If the application is not running, this method does nothing.
If the application is not running and this method is not called within
:meth:`post_init`, this method does nothing.
Warning:
This method is designed to for use in combination with :meth:`run_polling` or
:meth:`run_webhook`. Using this method in combination with a custom logic for starting
and stopping the application is not guaranteed to work as expected. Use at your own
risk.
.. versionadded:: 20.5
.. versionchanged:: 21.2
Added support for calling within :meth:`post_init`.
"""
if self.running:
# This works because `__run` is using `loop.run_forever()`. If that changes, this
# method needs to be adapted.
asyncio.get_running_loop().stop()
else:
_LOGGER.debug("Application is not running, stop_running() does nothing.")
self.__stop_running_marker.set()
if not self._initialized:
_LOGGER.debug(
"Application is not running and not initialized. `stop_running()` likely has "
"no effect."
)
def run_polling(
self,
@ -733,9 +766,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
polling updates from Telegram using :meth:`telegram.ext.Updater.start_polling` and
a graceful shutdown of the app on exit.
The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised.
On unix, the app will also shut down on receiving the signals specified by
:paramref:`stop_signals`.
|app_run_shutdown| :paramref:`stop_signals`.
The order of execution by :meth:`run_polling` is roughly as follows:
@ -826,9 +857,11 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
if (read_timeout, write_timeout, connect_timeout, pool_timeout) != ((DEFAULT_NONE,) * 4):
warn(
"Setting timeouts via `Application.run_polling` is deprecated. "
"Please use `ApplicationBuilder.get_updates_*_timeout` instead.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.6",
"Setting timeouts via `Application.run_polling` is deprecated. "
"Please use `ApplicationBuilder.get_updates_*_timeout` instead.",
),
stacklevel=2,
)
@ -874,9 +907,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
listening for updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and
a graceful shutdown of the app on exit.
The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised.
On unix, the app will also shut down on receiving the signals specified by
:paramref:`stop_signals`.
|app_run_shutdown| :paramref:`stop_signals`.
If :paramref:`cert`
and :paramref:`key` are not provided, the webhook will be started directly on
@ -1038,6 +1069,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
loop.run_until_complete(self.initialize())
if self.post_init:
loop.run_until_complete(self.post_init(self))
if self.__stop_running_marker.is_set():
_LOGGER.info("Application received stop signal via `stop_running`. Shutting down.")
return
loop.run_until_complete(updater_coroutine) # one of updater.start_webhook/polling
loop.run_until_complete(self.start())
loop.run_forever()
@ -1054,8 +1088,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
loop.run_until_complete(self.updater.stop()) # type: ignore[union-attr]
if self.running:
loop.run_until_complete(self.stop())
if self.post_stop:
loop.run_until_complete(self.post_stop(self))
# post_stop should be called only if stop was called!
if self.post_stop:
loop.run_until_complete(self.post_stop(self))
loop.run_until_complete(self.shutdown())
if self.post_shutdown:
loop.run_until_complete(self.post_shutdown(self))
@ -1150,9 +1185,11 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
# Generator-based coroutines are not supported in Python 3.12+
if sys.version_info < (3, 12) and isinstance(coroutine, Generator):
warn(
"Generator-based coroutines are deprecated in create_task and will not work"
" in Python 3.12+",
category=PTBDeprecationWarning,
PTBDeprecationWarning(
"20.4",
"Generator-based coroutines are deprecated in create_task and will not"
" work in Python 3.12+",
),
)
return await asyncio.create_task(coroutine)
# If user uses generator in python 3.12+, Exception will happen and we cannot do
@ -1184,45 +1221,44 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
finally:
self._mark_for_persistence_update(update=update)
async def _update_fetcher(self) -> None:
async def __update_fetcher(self) -> None:
# Continuously fetch updates from the queue. Exit only once the signal object is found.
while True:
try:
update = await self.update_queue.get()
update = await self.update_queue.get()
if update is _STOP_SIGNAL:
_LOGGER.debug("Dropping pending updates")
while not self.update_queue.empty():
self.update_queue.task_done()
if update is _STOP_SIGNAL:
# For the _STOP_SIGNAL
self.update_queue.task_done()
return
# For the _STOP_SIGNAL
self.update_queue.task_done()
return
_LOGGER.debug("Processing update %s", update)
_LOGGER.debug("Processing update %s", update)
if self._update_processor.max_concurrent_updates > 1:
# We don't await the below because it has to be run concurrently
self.create_task(
self.__process_update_wrapper(update),
update=update,
name=f"Application:{self.bot.id}:process_concurrent_update",
)
else:
await self.__process_update_wrapper(update)
except asyncio.CancelledError:
# This may happen if the application is manually run via application.start() and
# then a KeyboardInterrupt is sent. We must prevent this loop to die since
# application.stop() will wait for it's clean shutdown.
_LOGGER.warning(
"Fetching updates got a asyncio.CancelledError. Ignoring as this task may only"
"be closed via `Application.stop`."
if self._update_processor.max_concurrent_updates > 1:
# We don't await the below because it has to be run concurrently
self.create_task(
self.__process_update_wrapper(update),
update=update,
name=f"Application:{self.bot.id}:process_concurrent_update",
)
else:
await self.__process_update_wrapper(update)
async def _update_fetcher(self) -> None:
try:
await self.__update_fetcher()
finally:
while not self.update_queue.empty():
_LOGGER.debug("Dropping pending update: %s", self.update_queue.get_nowait())
with contextlib.suppress(ValueError):
# Since we're shutting down here, it's not too bad if we call task_done
# on an empty queue
self.update_queue.task_done()
async def __process_update_wrapper(self, update: object) -> None:
await self._update_processor.process_update(update, self.process_update(update))
self.update_queue.task_done()
try:
await self._update_processor.process_update(update, self.process_update(update))
finally:
self.update_queue.task_done()
async def process_update(self, update: object) -> None:
"""Processes a single update and marks the update to be updated by the persistence later.
@ -1251,30 +1287,43 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
try:
for handler in handlers:
check = handler.check_update(update) # Should the handler handle this update?
if not (check is None or check is False): # if yes,
if not context: # build a context if not already built
context = self.context_types.context.from_update(update, self)
await context.refresh_data()
coroutine: Coroutine = handler.handle_update(update, self, check, context)
if check is None or check is False:
continue
if not handler.block or ( # if handler is running with block=False,
handler.block is DEFAULT_TRUE
and isinstance(self.bot, ExtBot)
and self.bot.defaults
and not self.bot.defaults.block
):
self.create_task(
coroutine,
update=update,
name=(
f"Application:{self.bot.id}:process_update_non_blocking"
f":{handler}"
if not context: # build a context if not already built
try:
context = self.context_types.context.from_update(update, self)
except Exception as exc:
_LOGGER.critical(
(
"Error while building CallbackContext for update %s. "
"Update will not be processed."
),
update,
exc_info=exc,
)
else:
any_blocking = True
await coroutine
break # Only a max of 1 handler per group is handled
return
await context.refresh_data()
coroutine: Coroutine = handler.handle_update(update, self, check, context)
if not handler.block or ( # if handler is running with block=False,
handler.block is DEFAULT_TRUE
and isinstance(self.bot, ExtBot)
and self.bot.defaults
and not self.bot.defaults.block
):
self.create_task(
coroutine,
update=update,
name=(
f"Application:{self.bot.id}:process_update_non_blocking"
f":{handler}"
),
)
else:
any_blocking = True
await coroutine
break # Only a max of 1 handler per group is handled
# Stop processing with any other handler.
except ApplicationHandlerStop:
@ -1808,13 +1857,25 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
callback,
block,
) in self.error_handlers.items():
context = self.context_types.context.from_error(
update=update,
error=error,
application=self,
job=job,
coroutine=coroutine,
)
try:
context = self.context_types.context.from_error(
update=update,
error=error,
application=self,
job=job,
coroutine=coroutine,
)
except Exception as exc:
_LOGGER.critical(
(
"Error while building CallbackContext for exception %s. "
"Exception will not be processed by error handlers."
),
error,
exc_info=exc,
)
return False
if not block or ( # If error handler has `block=False`, create a Task to run cb
block is DEFAULT_TRUE
and isinstance(self.bot, ExtBot)

View file

@ -528,9 +528,11 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
warn(
"`ApplicationBuilder.proxy_url` is deprecated since version "
"20.7. Use `ApplicationBuilder.proxy` instead.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.7",
"`ApplicationBuilder.proxy_url` is deprecated. Use `ApplicationBuilder.proxy` "
"instead.",
),
stacklevel=2,
)
return self.proxy(proxy_url)
@ -760,9 +762,11 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
warn(
"`ApplicationBuilder.get_updates_proxy_url` is deprecated since version "
"20.7. Use `ApplicationBuilder.get_updates_proxy` instead.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.7",
"`ApplicationBuilder.get_updates_proxy_url` is deprecated. Use "
"`ApplicationBuilder.get_updates_proxy` instead.",
),
stacklevel=2,
)
return self.get_updates_proxy(get_updates_proxy_url)
@ -1334,7 +1338,13 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Tip:
This can be used for custom stop logic that requires to await coroutines, e.g.
sending message to a chat before shutting down the bot
sending message to a chat before shutting down the bot.
Hint:
The callback will be called only, if :meth:`Application.stop` was indeed called
successfully. For example, if the application is stopped early by calling
:meth:`Application.stop_running` within :meth:`post_init`, then the set callback will
*not* be called.
Example:
.. code::

View file

@ -156,9 +156,11 @@ class Defaults:
raise ValueError("`quote` and `do_quote` are mutually exclusive")
if disable_web_page_preview is not None:
warn(
"`Defaults.disable_web_page_preview` is deprecated. Use "
"`Defaults.link_preview_options` instead.",
category=PTBDeprecationWarning,
PTBDeprecationWarning(
"20.8",
"`Defaults.disable_web_page_preview` is deprecated. Use "
"`Defaults.link_preview_options` instead.",
),
stacklevel=2,
)
self._link_preview_options: Optional[LinkPreviewOptions] = LinkPreviewOptions(
@ -169,8 +171,9 @@ class Defaults:
if quote is not None:
warn(
"`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead.",
category=PTBDeprecationWarning,
PTBDeprecationWarning(
"20.8", "`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead."
),
stacklevel=2,
)
self._do_quote: Optional[bool] = quote
@ -179,13 +182,14 @@ class Defaults:
# Gather all defaults that actually have a default value
self._api_defaults = {}
for kwarg in (
"parse_mode",
"explanation_parse_mode",
"disable_notification",
"allow_sending_without_reply",
"protect_content",
"link_preview_options",
"disable_notification",
"do_quote",
"explanation_parse_mode",
"link_preview_options",
"parse_mode",
"protect_content",
"question_parse_mode",
):
value = getattr(self, kwarg)
if value is not None:
@ -264,6 +268,36 @@ class Defaults:
"You can not assign a new value to quote_parse_mode after initialization."
)
@property
def text_parse_mode(self) -> Optional[str]:
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
the corresponding parameter of :class:`telegram.InputPollOption`.
.. versionadded:: 21.2
"""
return self._parse_mode
@text_parse_mode.setter
def text_parse_mode(self, _: object) -> NoReturn:
raise AttributeError(
"You can not assign a new value to text_parse_mode after initialization."
)
@property
def question_parse_mode(self) -> Optional[str]:
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
the corresponding parameter of :meth:`telegram.Bot.send_poll`.
.. versionadded:: 21.2
"""
return self._parse_mode
@question_parse_mode.setter
def question_parse_mode(self, _: object) -> NoReturn:
raise AttributeError(
"You can not assign a new value to question_parse_mode after initialization."
)
@property
def disable_notification(self) -> Optional[bool]:
""":obj:`bool`: Optional. Sends the message silently. Users will

View file

@ -50,8 +50,8 @@ from telegram import (
BotShortDescription,
BusinessConnection,
CallbackQuery,
Chat,
ChatAdministratorRights,
ChatFullInfo,
ChatInviteLink,
ChatMember,
ChatPermissions,
@ -64,6 +64,7 @@ from telegram import (
InlineKeyboardMarkup,
InlineQueryResultsButton,
InputMedia,
InputPollOption,
LinkPreviewOptions,
Location,
MaskPosition,
@ -113,7 +114,7 @@ if TYPE_CHECKING:
)
from telegram.ext import BaseRateLimiter, Defaults
HandledTypes = TypeVar("HandledTypes", bound=Union[Message, CallbackQuery, Chat])
HandledTypes = TypeVar("HandledTypes", bound=Union[Message, CallbackQuery, ChatFullInfo])
KT = TypeVar("KT", bound=ReplyMarkup)
@ -262,7 +263,10 @@ class ExtBot(Bot, Generic[RLARGS]):
@classmethod
def _warn(
cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0
cls,
message: Union[str, PTBUserWarning],
category: Type[Warning] = PTBUserWarning,
stacklevel: int = 0,
) -> None:
"""We override this method to add one more level to the stacklevel, so that the warning
points to the user's code, not to the PTB code.
@ -436,6 +440,7 @@ class ExtBot(Bot, Generic[RLARGS]):
# 3) set the correct parse_mode for all InputMedia objects
# 4) handle the LinkPreviewOptions case (see below)
# 5) handle the ReplyParameters case (see below)
# 6) handle text_parse_mode in InputPollOption
for key, val in data.items():
# 1)
if isinstance(val, DefaultValue):
@ -487,6 +492,21 @@ class ExtBot(Bot, Generic[RLARGS]):
data[key] = new_value
# 6)
elif isinstance(val, Sequence) and all(
isinstance(obj, InputPollOption) for obj in val
):
new_val = []
for option in val:
if not isinstance(option.text_parse_mode, DefaultValue):
new_val.append(option)
else:
new_option = copy(option)
with new_option._unfrozen():
new_option.text_parse_mode = self.defaults.text_parse_mode
new_val.append(new_option)
data[key] = new_val
def _replace_keyboard(self, reply_markup: Optional[KT]) -> Optional[KT]:
# If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the
# CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input
@ -554,7 +574,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self.callback_data_cache.process_message(message=obj)
return obj # type: ignore[return-value]
if isinstance(obj, Chat) and obj.pinned_message:
if isinstance(obj, ChatFullInfo) and obj.pinned_message:
self.callback_data_cache.process_message(obj.pinned_message)
return obj
@ -853,7 +873,7 @@ class ExtBot(Bot, Generic[RLARGS]):
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> Chat:
) -> ChatFullInfo:
# We override this method to call self._insert_callback_data
result = await super().get_chat(
chat_id=chat_id,
@ -1520,6 +1540,7 @@ class ExtBot(Bot, Generic[RLARGS]):
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
live_period: Optional[int] = None,
*,
location: Optional[Location] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -1539,6 +1560,7 @@ class ExtBot(Bot, Generic[RLARGS]):
horizontal_accuracy=horizontal_accuracy,
heading=heading,
proximity_alert_radius=proximity_alert_radius,
live_period=live_period,
location=location,
read_timeout=read_timeout,
write_timeout=write_timeout,
@ -2915,7 +2937,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
chat_id: Union[int, str],
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None, # pylint: disable=redefined-builtin
allows_multiple_answers: Optional[bool] = None,
@ -2932,6 +2954,8 @@ class ExtBot(Bot, Generic[RLARGS]):
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@ -2969,6 +2993,8 @@ class ExtBot(Bot, Generic[RLARGS]):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
question_parse_mode=question_parse_mode,
question_entities=question_entities,
)
async def send_sticker(

View file

@ -49,7 +49,7 @@ class StringCommandHandler(BaseHandler[str, CCT]):
called when :meth:`check_update` has determined that an update should be processed by
this handler. Callback signature::
async def callback(update: Update, context: CallbackContext)
async def callback(update: str, context: CallbackContext)
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.

View file

@ -52,7 +52,7 @@ class StringRegexHandler(BaseHandler[str, CCT]):
called when :meth:`check_update` has determined that an update should be processed by
this handler. Callback signature::
async def callback(update: Update, context: CallbackContext)
async def callback(update: str, context: CallbackContext)
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.

View file

@ -43,7 +43,7 @@ class TypeHandler(BaseHandler[UT, CCT]):
called when :meth:`check_update` has determined that an update should be processed by
this handler. Callback signature::
async def callback(update: Update, context: CallbackContext)
async def callback(update: object, context: CallbackContext)
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.

View file

@ -31,6 +31,7 @@ try:
except ImportError:
APS_AVAILABLE = False
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import JSONDict
from telegram.ext._extbot import ExtBot
@ -44,6 +45,7 @@ if TYPE_CHECKING:
_ALL_DAYS = tuple(range(7))
_LOGGER = get_logger(__name__, class_name="JobQueue")
class JobQueue(Generic[CCT]):
@ -953,7 +955,16 @@ class Job(Generic[CCT]):
self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]"
) -> None:
try:
context = application.context_types.context.from_job(self, application)
try:
context = application.context_types.context.from_job(self, application)
except Exception as exc:
_LOGGER.critical(
"Error while building CallbackContext for job %s. Job will not be run.",
self._job,
exc_info=exc,
)
return
await context.refresh_data()
await self.callback(context)
except Exception as exc:

View file

@ -1909,7 +1909,8 @@ class StatusUpdate:
def filter(self, update: Update) -> bool:
return bool(
# keep this alphabetically sorted for easier maintenance
StatusUpdate.CHAT_CREATED.check_update(update)
StatusUpdate.CHAT_BACKGROUND_SET.check_update(update)
or StatusUpdate.CHAT_CREATED.check_update(update)
or StatusUpdate.CHAT_SHARED.check_update(update)
or StatusUpdate.CONNECTED_WEBSITE.check_update(update)
or StatusUpdate.DELETE_CHAT_PHOTO.check_update(update)
@ -1942,6 +1943,15 @@ class StatusUpdate:
ALL = _All(name="filters.StatusUpdate.ALL")
"""Messages that contain any of the below."""
class _ChatBackgroundSet(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.chat_background_set)
CHAT_BACKGROUND_SET = _ChatBackgroundSet(name="filters.StatusUpdate.CHAT_BACKGROUND_SET")
"""Messages that contain :attr:`telegram.Message.chat_background_set`."""
class _ChatCreated(MessageFilter):
__slots__ = ()

View file

@ -318,10 +318,12 @@ class BaseRequest(
and isinstance(write_timeout, DefaultValue)
):
warn(
f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request "
"will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions "
"for *all* methods of the `Bot` class, including methods sending media.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.7",
f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request"
" will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions "
"for *all* methods of the `Bot` class, including methods sending media.",
),
stacklevel=3,
)
write_timeout = 20

View file

@ -146,9 +146,9 @@ class HTTPXRequest(BaseRequest):
if proxy_url is not None:
proxy = proxy_url
warn(
"The parameter `proxy_url` is deprecated since version 20.7. Use `proxy` "
"instead.",
PTBDeprecationWarning,
PTBDeprecationWarning(
"20.7", "The parameter `proxy_url` is deprecated. Use `proxy` instead."
),
stacklevel=2,
)

View file

@ -54,6 +54,34 @@ class PTBDeprecationWarning(PTBUserWarning, DeprecationWarning):
.. versionchanged:: 20.0
Renamed TelegramDeprecationWarning to PTBDeprecationWarning.
Args:
version (:obj:`str`): The version in which the feature was deprecated.
.. versionadded:: 21.2
message (:obj:`str`): The message to display.
.. versionadded:: 21.2
Attributes:
version (:obj:`str`): The version in which the feature was deprecated.
.. versionadded:: 21.2
message (:obj:`str`): The message to display.
.. versionadded:: 21.2
"""
__slots__ = ()
__slots__ = ("message", "version")
def __init__(self, version: str, message: str) -> None:
self.version: str = version
self.message: str = message
def __str__(self) -> str:
"""Returns a string representation of the warning, using :attr:`message` and
:attr:`version`.
.. versionadded:: 21.2
"""
return f"Deprecated since version {self.version}: {self.message}"

View file

@ -124,7 +124,8 @@ class TestLocationWithoutRequest(TestLocationBase):
ha = data["horizontal_accuracy"] == "50"
heading = data["heading"] == "90"
prox_alert = data["proximity_alert_radius"] == "1000"
return lat and lon and id_ and ha and heading and prox_alert
live = data["live_period"] == "900"
return lat and lon and id_ and ha and heading and prox_alert and live
monkeypatch.setattr(bot.request, "post", make_assertion)
assert await bot.edit_message_live_location(
@ -133,6 +134,7 @@ class TestLocationWithoutRequest(TestLocationBase):
horizontal_accuracy=50,
heading=90,
proximity_alert_radius=1000,
live_period=900,
)
# TODO: Needs improvement with in inline sent live location.
@ -262,6 +264,7 @@ class TestLocationWithRequest:
horizontal_accuracy=30,
heading=10,
proximity_alert_radius=500,
live_period=200,
)
assert pytest.approx(message2.location.latitude, rel=1e-5) == 52.223098
@ -269,6 +272,7 @@ class TestLocationWithRequest:
assert message2.location.horizontal_accuracy == 30
assert message2.location.heading == 10
assert message2.location.proximity_alert_radius == 500
assert message2.location.live_period == 200
await bot.stop_message_live_location(message.chat_id, message.message_id)
with pytest.raises(BadRequest, match="Message can't be edited"):

View file

@ -314,6 +314,8 @@ def build_kwargs(
elif name == "ok":
kws["ok"] = False
kws["error_message"] = "error"
elif name == "options":
kws[name] = ["option1", "option2"]
else:
kws[name] = True

View file

@ -19,6 +19,7 @@
"""The integration of persistence into the application is tested in test_basepersistence.
"""
import asyncio
import functools
import inspect
import logging
import os
@ -2083,75 +2084,174 @@ class TestApplication:
assert set(self.received.keys()) == set(expected.keys())
assert self.received == expected
@pytest.mark.skipif(
platform.system() == "Windows",
reason="Can't send signals without stopping whole process on windows",
)
async def test_cancellation_error_does_not_stop_polling(
self, one_time_bot, monkeypatch, caplog
@pytest.mark.parametrize("exception", [SystemExit, KeyboardInterrupt])
def test_raise_system_exit_keyboard_interrupt_post_init(
self, one_time_bot, monkeypatch, exception
):
"""
Ensures that hitting CTRL+C while polling *without* run_polling doesn't kill
the update_fetcher loop such that a shutdown is still possible.
This test is far from perfect, but it's the closest we can come with sane effort.
"""
async def post_init(application):
raise exception
called_callbacks = set()
async def callback(*args, **kwargs):
called_callbacks.add(kwargs["name"])
for cls, method, entry in [
(Application, "initialize", "app_initialize"),
(Application, "start", "app_start"),
(Application, "stop", "app_stop"),
(Application, "shutdown", "app_shutdown"),
(Updater, "initialize", "updater_initialize"),
(Updater, "shutdown", "updater_shutdown"),
(Updater, "stop", "updater_stop"),
(Updater, "start_polling", "updater_start_polling"),
]:
def after(_, name):
called_callbacks.add(name)
monkeypatch.setattr(
cls,
method,
call_after(getattr(cls, method), functools.partial(after, name=entry)),
)
app = (
ApplicationBuilder()
.bot(one_time_bot)
.post_init(post_init)
.post_stop(functools.partial(callback, name="post_stop"))
.post_shutdown(functools.partial(callback, name="post_shutdown"))
.build()
)
app.run_polling(close_loop=False)
# This checks two things:
# 1. start/stop are *not* called!
# 2. we do have a graceful shutdown
assert called_callbacks == {
"app_initialize",
"updater_initialize",
"app_shutdown",
"post_shutdown",
"updater_shutdown",
}
@pytest.mark.parametrize("exception", [SystemExit("PTBTest"), KeyboardInterrupt("PTBTest")])
@pytest.mark.parametrize("kind", ["handler", "error_handler", "job"])
# @pytest.mark.parametrize("block", [True, False])
# Testing with block=False would be nice but that doesn't work well with pytest for some reason
# in any case, block=False is the simpler behavior since it is roughly similar to what happens
# when you hit CTRL+C in the commandline.
def test_raise_system_exit_keyboard_jobs_handlers(
self, one_time_bot, monkeypatch, exception, kind, caplog
):
async def queue_and_raise(application):
await application.update_queue.put("will_not_be_processed")
raise exception
async def handler_callback(update, context):
if kind == "handler":
await queue_and_raise(context.application)
elif kind == "error_handler":
raise TelegramError("Triggering error callback")
async def error_callback(update, context):
await queue_and_raise(context.application)
async def job_callback(context):
await queue_and_raise(context.application)
async def enqueue_update():
await asyncio.sleep(0.5)
await app.update_queue.put(1)
async def post_init(application):
if kind == "job":
application.job_queue.run_once(when=0.5, callback=job_callback)
else:
app.create_task(enqueue_update())
async def update_logger_callback(update, context):
context.bot_data.setdefault("processed_updates", set()).add(update)
called_callbacks = set()
async def callback(*args, **kwargs):
called_callbacks.add(kwargs["name"])
async def get_updates(*args, **kwargs):
await asyncio.sleep(0)
return [None]
return []
monkeypatch.setattr(one_time_bot, "get_updates", get_updates)
app = ApplicationBuilder().bot(one_time_bot).build()
for cls, method, entry in [
(Application, "initialize", "app_initialize"),
(Application, "start", "app_start"),
(Application, "stop", "app_stop"),
(Application, "shutdown", "app_shutdown"),
(Updater, "initialize", "updater_initialize"),
(Updater, "shutdown", "updater_shutdown"),
(Updater, "stop", "updater_stop"),
(Updater, "start_polling", "updater_start_polling"),
]:
original_get = app.update_queue.get
raise_cancelled_error = threading.Event()
def after(_, name):
called_callbacks.add(name)
async def get(*arg, **kwargs):
await asyncio.sleep(0.05)
if raise_cancelled_error.is_set():
raise_cancelled_error.clear()
raise asyncio.CancelledError("Mocked CancelledError")
return await original_get(*arg, **kwargs)
monkeypatch.setattr(
cls,
method,
call_after(getattr(cls, method), functools.partial(after, name=entry)),
)
monkeypatch.setattr(app.update_queue, "get", get)
def thread_target():
waited = 0
while not app.running:
time.sleep(0.05)
waited += 0.05
if waited > 5:
pytest.fail("App apparently won't start")
time.sleep(0.1)
raise_cancelled_error.set()
async with app:
with caplog.at_level(logging.WARNING):
thread = Thread(target=thread_target)
await app.start()
thread.start()
assert thread.is_alive()
raise_cancelled_error.wait()
# The exit should have been caught and the app should still be running
assert not thread.is_alive()
assert app.running
# Explicit shutdown is required
await app.stop()
thread.join()
assert not thread.is_alive()
assert not app.running
# Make sure that we were warned about the necessity of a manual shutdown
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.name == "telegram.ext.Application"
assert record.getMessage().startswith(
"Fetching updates got a asyncio.CancelledError. Ignoring"
app = (
ApplicationBuilder()
.bot(one_time_bot)
.post_init(post_init)
.post_stop(functools.partial(callback, name="post_stop"))
.post_shutdown(functools.partial(callback, name="post_shutdown"))
.build()
)
monkeypatch.setattr(app.bot, "get_updates", get_updates)
app.add_handler(TypeHandler(object, update_logger_callback), group=-10)
app.add_handler(TypeHandler(object, handler_callback))
app.add_error_handler(error_callback)
with caplog.at_level(logging.DEBUG):
app.run_polling(close_loop=False)
# This checks that we have a clean shutdown even when the user raises SystemExit
# or KeyboardInterrupt in a handler/error handler/job callback
assert called_callbacks == {
"app_initialize",
"app_shutdown",
"app_start",
"app_stop",
"post_shutdown",
"post_stop",
"updater_initialize",
"updater_shutdown",
"updater_start_polling",
"updater_stop",
}
# These next checks make sure that the update queue is properly cleaned even if there are
# still pending updates in the queue
# Unfortunately this is apparently extremely hard to get right with jobs, so we're
# skipping that case for the sake of simplicity
if kind == "job":
return
found = False
for record in caplog.records:
if record.getMessage() != "Dropping pending update: will_not_be_processed":
continue
assert record.name == "telegram.ext.Application"
assert record.levelno == logging.DEBUG
found = True
assert found, "`Dropping pending updates` message not found in logs!"
assert "will_not_be_processed" not in app.bot_data.get("processed_updates", set())
def test_run_without_updater(self, one_time_bot):
app = ApplicationBuilder().bot(one_time_bot).updater(None).build()
@ -2311,7 +2411,44 @@ class TestApplication:
assert len(caplog.records) == 1
assert caplog.records[-1].name == "telegram.ext.Application"
assert caplog.records[-1].getMessage().endswith("stop_running() does nothing.")
assert caplog.records[-1].getMessage().endswith("`stop_running()` likely has no effect.")
def test_stop_running_post_init(self, app, monkeypatch, caplog, one_time_bot):
async def post_init(app):
app.stop_running()
called_callbacks = []
async def callback(*args, **kwargs):
called_callbacks.append(kwargs["name"])
monkeypatch.setattr(Application, "start", functools.partial(callback, name="start"))
monkeypatch.setattr(
Updater, "start_polling", functools.partial(callback, name="start_polling")
)
app = (
ApplicationBuilder()
.bot(one_time_bot)
.post_init(post_init)
.post_stop(functools.partial(callback, name="post_stop"))
.post_shutdown(functools.partial(callback, name="post_shutdown"))
.build()
)
with caplog.at_level(logging.INFO):
app.run_polling(close_loop=False)
# The important part here is that start(_polling) are *not* called!
# post_stop must not be called either, since we never called stop()
assert called_callbacks == ["post_shutdown"]
assert len(caplog.records) == 1
assert caplog.records[-1].name == "telegram.ext.Application"
assert (
"Application received stop signal via `stop_running`"
in caplog.records[-1].getMessage()
)
@pytest.mark.parametrize("method", ["polling", "webhook"])
def test_stop_running(self, one_time_bot, monkeypatch, method):
@ -2421,3 +2558,83 @@ class TestApplication:
assert len(assertions) == 5
for key, value in assertions.items():
assert value, f"assertion '{key}' failed!"
async def test_process_update_exception_in_building_context(self, monkeypatch, caplog, app):
# Makes sure that exceptions in building the context don't stop the application
exception = ValueError("TestException")
original_from_update = CallbackContext.from_update
def raise_exception(update, application):
if update == 1:
raise exception
return original_from_update(update, application)
monkeypatch.setattr(CallbackContext, "from_update", raise_exception)
received_updates = set()
async def callback(update, context):
received_updates.add(update)
app.add_handler(TypeHandler(int, callback))
async with app:
with caplog.at_level(logging.CRITICAL):
await app.process_update(1)
assert received_updates == set()
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.name == "telegram.ext.Application"
assert record.getMessage().startswith(
"Error while building CallbackContext for update 1"
)
assert record.levelno == logging.CRITICAL
# Let's also check that no critical log is produced when the exception is not raised
caplog.clear()
with caplog.at_level(logging.CRITICAL):
await app.process_update(2)
assert received_updates == {2}
assert len(caplog.records) == 0
async def test_process_error_exception_in_building_context(self, monkeypatch, caplog, app):
# Makes sure that exceptions in building the context don't stop the application
exception = ValueError("TestException")
original_from_error = CallbackContext.from_error
def raise_exception(update, error, application, *args, **kwargs):
if error == 1:
raise exception
return original_from_error(update, error, application, *args, **kwargs)
monkeypatch.setattr(CallbackContext, "from_error", raise_exception)
received_errors = set()
async def callback(update, context):
received_errors.add(context.error)
app.add_error_handler(callback)
async with app:
with caplog.at_level(logging.CRITICAL):
await app.process_error(update=None, error=1)
assert received_errors == set()
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.name == "telegram.ext.Application"
assert record.getMessage().startswith(
"Error while building CallbackContext for exception 1"
)
assert record.levelno == logging.CRITICAL
# Let's also check that no critical log is produced when the exception is not raised
caplog.clear()
with caplog.at_level(logging.CRITICAL):
await app.process_error(update=None, error=2)
assert received_errors == {2}
assert len(caplog.records) == 0

View file

@ -1090,6 +1090,11 @@ class TestFilters:
assert filters.StatusUpdate.GIVEAWAY_COMPLETED.check_update(update)
update.message.giveaway_completed = None
update.message.chat_background_set = "test_background"
assert filters.StatusUpdate.ALL.check_update(update)
assert filters.StatusUpdate.CHAT_BACKGROUND_SET.check_update(update)
update.message.chat_background_set = None
def test_filters_forwarded(self, update, message_origin_user):
assert filters.FORWARDED.check_update(update)
update.message.forward_origin = MessageOriginHiddenUser(datetime.datetime.utcnow(), 1)

View file

@ -646,3 +646,44 @@ class TestJobQueue:
tg_job = Job.from_aps_job(aps_job)
assert tg_job is job
assert tg_job.job is aps_job
async def test_run_job_exception_in_building_context(
self, monkeypatch, job_queue, caplog, app
):
# Makes sure that exceptions in building the context don't stop the application
exception = ValueError("TestException")
original_from_job = CallbackContext.from_job
def raise_exception(job, application):
if job.data == 1:
raise exception
return original_from_job(job, application)
monkeypatch.setattr(CallbackContext, "from_job", raise_exception)
received_jobs = set()
async def job_callback(context):
received_jobs.add(context.job.data)
with caplog.at_level(logging.CRITICAL):
job_queue.run_once(job_callback, 0.1, data=1)
await asyncio.sleep(0.2)
assert received_jobs == set()
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.name == "telegram.ext.JobQueue"
assert record.getMessage().startswith(
"Error while building CallbackContext for job job_callback"
)
assert record.levelno == logging.CRITICAL
# Let's also check that no critical log is produced when the exception is not raised
caplog.clear()
with caplog.at_level(logging.CRITICAL):
job_queue.run_once(job_callback, 0.1, data=2)
await asyncio.sleep(0.2)
assert received_jobs == {2}
assert len(caplog.records) == 0

View file

@ -43,6 +43,7 @@ from telegram import (
CallbackQuery,
Chat,
ChatAdministratorRights,
ChatFullInfo,
ChatPermissions,
Dice,
InlineKeyboardButton,
@ -55,6 +56,7 @@ from telegram import (
InputMediaDocument,
InputMediaPhoto,
InputMessageContent,
InputPollOption,
InputTextMessageContent,
LabeledPrice,
LinkPreviewOptions,
@ -1936,6 +1938,59 @@ class TestBotWithoutRequest:
chat_id, message, reply_parameters=ReplyParameters(**kwargs)
)
@pytest.mark.parametrize(
("default_bot", "custom"),
[
({"parse_mode": ParseMode.HTML}, "NOTHING"),
({"parse_mode": ParseMode.HTML}, None),
({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2),
({"parse_mode": None}, ParseMode.MARKDOWN_V2),
],
indirect=["default_bot"],
)
async def test_send_poll_default_text_question_parse_mode(
self, default_bot, raw_bot, chat_id, custom, monkeypatch
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
expected = default_bot.defaults.text_parse_mode if custom == "NOTHING" else custom
option_1 = request_data.parameters["options"][0]
option_2 = request_data.parameters["options"][1]
assert option_1.get("text_parse_mode") == (default_bot.defaults.text_parse_mode)
assert option_2.get("text_parse_mode") == expected
assert request_data.parameters.get("question_parse_mode") == expected
return make_message("dummy reply").to_dict()
async def make_raw_assertion(url, request_data: RequestData, *args, **kwargs):
expected = None if custom == "NOTHING" else custom
option_1 = request_data.parameters["options"][0]
option_2 = request_data.parameters["options"][1]
assert option_1.get("text_parse_mode") is None
assert option_2.get("text_parse_mode") == expected
assert request_data.parameters.get("question_parse_mode") == expected
return make_message("dummy reply").to_dict()
if custom == "NOTHING":
option_2 = InputPollOption("option2")
kwargs = {}
else:
option_2 = InputPollOption("option2", text_parse_mode=custom)
kwargs = {"question_parse_mode": custom}
monkeypatch.setattr(default_bot.request, "post", make_assertion)
await default_bot.send_poll(
chat_id, question="question", options=["option1", option_2], **kwargs
)
monkeypatch.setattr(raw_bot.request, "post", make_raw_assertion)
await raw_bot.send_poll(
chat_id, question="question", options=["option1", option_2], **kwargs
)
@pytest.mark.parametrize(
("default_bot", "custom"),
[
@ -1966,6 +2021,30 @@ class TestBotWithoutRequest:
reply_parameters=ReplyParameters(**kwargs),
)
async def test_send_poll_question_parse_mode_entities(self, bot, monkeypatch):
# Currently only custom emoji are supported as entities which we can't test
# We just test that the correct data is passed for now
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters["question_entities"] == [
{"type": "custom_emoji", "offset": 0, "length": 1},
{"type": "custom_emoji", "offset": 2, "length": 1},
]
assert request_data.parameters["question_parse_mode"] == ParseMode.MARKDOWN_V2
return make_message("dummy reply").to_dict()
monkeypatch.setattr(bot.request, "post", make_assertion)
await bot.send_poll(
1,
question="😀😃",
options=["option1", "option2"],
question_entities=[
MessageEntity(MessageEntity.CUSTOM_EMOJI, 0, 1),
MessageEntity(MessageEntity.CUSTOM_EMOJI, 2, 1),
],
question_parse_mode=ParseMode.MARKDOWN_V2,
)
@pytest.mark.parametrize(
("default_bot", "custom"),
[
@ -2025,6 +2104,7 @@ class TestBotWithoutRequest:
monkeypatch.setattr(bot.request, "post", make_assertion)
assert await bot.do_api_request("camel_case")
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
async def test_do_api_request_media_write_timeout(self, bot, chat_id, monkeypatch):
test_flag = None
@ -2063,6 +2143,7 @@ class TestBotWithoutRequest:
DEFAULT_NONE,
)
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
async def test_do_api_request_default_timezone(self, tz_bot, monkeypatch):
until = dtm.datetime(2020, 1, 11, 16, 13)
until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo)
@ -2324,7 +2405,7 @@ class TestBotWithRequest:
)
async def test_send_and_stop_poll(self, bot, super_group_id, reply_markup):
question = "Is this a test?"
answers = ["Yes", "No", "Maybe"]
answers = ["Yes", InputPollOption("No"), "Maybe"]
explanation = "[Here is a link](https://google.com)"
explanation_entities = [
MessageEntity(MessageEntity.TEXT_LINK, 0, 14, url="https://google.com")
@ -2358,7 +2439,7 @@ class TestBotWithRequest:
assert message.poll
assert message.poll.question == question
assert message.poll.options[0].text == answers[0]
assert message.poll.options[1].text == answers[1]
assert message.poll.options[1].text == answers[1].text
assert message.poll.options[2].text == answers[2]
assert not message.poll.is_anonymous
assert message.poll.allows_multiple_answers
@ -2378,7 +2459,7 @@ class TestBotWithRequest:
assert poll.is_closed
assert poll.options[0].text == answers[0]
assert poll.options[0].voter_count == 0
assert poll.options[1].text == answers[1]
assert poll.options[1].text == answers[1].text
assert poll.options[1].voter_count == 0
assert poll.options[2].text == answers[2]
assert poll.options[2].voter_count == 0
@ -2921,10 +3002,10 @@ class TestBotWithRequest:
await bot.leave_chat(-123456)
async def test_get_chat(self, bot, super_group_id):
chat = await bot.get_chat(super_group_id)
assert chat.type == "supergroup"
assert chat.title == f">>> telegram.Bot(test) @{bot.username}"
assert chat.id == int(super_group_id)
cfi = await bot.get_chat(super_group_id)
assert cfi.type == "supergroup"
assert cfi.title == f">>> telegram.Bot(test) @{bot.username}"
assert cfi.id == int(super_group_id)
async def test_get_chat_administrators(self, bot, channel_id):
admins = await bot.get_chat_administrators(channel_id)
@ -3916,9 +3997,9 @@ class TestBotWithRequest:
)
assert data == "callback_data"
chat = await bot.get_chat(channel_id)
assert chat.pinned_message == message
assert chat.pinned_message.reply_markup == reply_markup
cfi = await bot.get_chat(channel_id)
assert cfi.pinned_message == message
assert cfi.pinned_message.reply_markup == reply_markup
assert await message.unpin() # (not placed in finally block since msg can be unbound)
finally:
bot.callback_data_cache.clear_callback_data()
@ -3931,11 +4012,11 @@ class TestBotWithRequest:
await bot.unpin_all_chat_messages(super_group_id)
try:
chat = await bot.get_chat(super_group_id)
cfi = await bot.get_chat(super_group_id)
assert isinstance(chat, Chat)
assert int(chat.id) == int(super_group_id)
assert chat.pinned_message is None
assert isinstance(cfi, ChatFullInfo)
assert int(cfi.id) == int(super_group_id)
assert cfi.pinned_message is None
finally:
bot.callback_data_cache.clear_callback_data()
bot.callback_data_cache.clear_callback_queries()
@ -4027,7 +4108,7 @@ class TestBotWithRequest:
@pytest.mark.parametrize("bot_class", [Bot, ExtBot])
async def test_do_api_request_warning_known_method(self, bot, bot_class):
with pytest.warns(PTBDeprecationWarning, match="Please use 'Bot.get_me'") as record:
with pytest.warns(PTBUserWarning, match="Please use 'Bot.get_me'") as record:
await bot_class(bot.token).do_api_request("get_me")
assert record[0].filename == __file__, "Wrong stack level!"
@ -4036,6 +4117,7 @@ class TestBotWithRequest:
with pytest.raises(EndPointNotFound, match="'unknownEndpoint' not found"):
await bot.do_api_request("unknown_endpoint")
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
async def test_do_api_request_invalid_token(self, bot):
# we do not initialize the bot here on purpose b/c that's the case were we actually
# do not know for sure if the token is invalid or the method was not found
@ -4050,6 +4132,7 @@ class TestBotWithRequest:
):
await Bot(bot.token).do_api_request("unknown_endpoint")
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
@pytest.mark.parametrize("return_type", [Message, None])
async def test_do_api_request_basic_and_files(self, bot, chat_id, return_type):
result = await bot.do_api_request(
@ -4074,6 +4157,7 @@ class TestBotWithRequest:
assert out.read() == data_file("telegram.png").open("rb").read()
assert result.document.file_name == "telegram.png"
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
@pytest.mark.parametrize("return_type", [Message, None])
async def test_do_api_request_list_return_type(self, bot, chat_id, return_type):
result = await bot.do_api_request(
@ -4112,6 +4196,7 @@ class TestBotWithRequest:
assert out.read() == data_file(file_name).open("rb").read()
assert message.document.file_name == file_name
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
@pytest.mark.parametrize("return_type", [Message, None])
async def test_do_api_request_bool_return_type(self, bot, chat_id, return_type):
assert await bot.do_api_request("delete_my_commands", return_type=return_type) is True

View file

@ -301,8 +301,9 @@ class TestCallbackQueryWithoutRequest(TestCallbackQueryBase):
async def make_assertion(*_, **kwargs):
latitude = kwargs.get("latitude") == 1
longitude = kwargs.get("longitude") == 2
live = kwargs.get("live_period") == 900
ids = self.check_passed_ids(callback_query, kwargs)
return ids and latitude and longitude
return ids and latitude and longitude and live
assert check_shortcut_signature(
CallbackQuery.edit_message_live_location,
@ -322,8 +323,10 @@ class TestCallbackQueryWithoutRequest(TestCallbackQueryBase):
)
monkeypatch.setattr(callback_query.get_bot(), "edit_message_live_location", make_assertion)
assert await callback_query.edit_message_live_location(latitude=1, longitude=2)
assert await callback_query.edit_message_live_location(1, 2)
assert await callback_query.edit_message_live_location(
latitude=1, longitude=2, live_period=900
)
assert await callback_query.edit_message_live_location(1, 2, live_period=900)
async def test_stop_message_live_location(self, monkeypatch, callback_query):
if isinstance(callback_query.message, InaccessibleMessage):

View file

@ -16,7 +16,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/].
import datetime
import warnings
import pytest
@ -35,9 +37,11 @@ from telegram import (
ReactionTypeEmoji,
User,
)
from telegram._chat import _deprecated_attrs
from telegram._utils.datetime import UTC, to_timestamp
from telegram.constants import ChatAction, ChatType, ReactionEmoji
from telegram.helpers import escape_markdown
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.bot_method_checks import (
check_defaults_handling,
check_shortcut_call,
@ -84,6 +88,8 @@ def chat(bot):
business_opening_hours=TestChatBase.business_opening_hours,
birthdate=Birthdate(1, 1),
personal_chat=TestChatBase.personal_chat,
first_name=TestChatBase.first_name,
last_name=TestChatBase.last_name,
)
chat.set_bot(bot)
chat._unfreeze()
@ -137,6 +143,8 @@ class TestChatBase:
custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name"
birthdate = Birthdate(1, 1)
personal_chat = Chat(3, "private", "private")
first_name = "first"
last_name = "last"
class TestChatWithoutRequest(TestChatBase):
@ -185,6 +193,8 @@ class TestChatWithoutRequest(TestChatBase):
"custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name,
"birthdate": self.birthdate.to_dict(),
"personal_chat": self.personal_chat.to_dict(),
"first_name": self.first_name,
"last_name": self.last_name,
}
chat = Chat.de_json(json_dict, bot)
@ -230,6 +240,8 @@ class TestChatWithoutRequest(TestChatBase):
assert chat.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name
assert chat.birthdate == self.birthdate
assert chat.personal_chat == self.personal_chat
assert chat.first_name == self.first_name
assert chat.last_name == self.last_name
def test_de_json_localization(self, bot, raw_bot, tz_bot):
json_dict = {
@ -251,6 +263,15 @@ class TestChatWithoutRequest(TestChatBase):
assert chat_bot_raw.emoji_status_expiration_date.tzinfo == UTC
assert emoji_expire_offset_tz == emoji_expire_offset
def test_always_tuples_attributes(self):
chat = Chat(
id=123,
title="title",
type=Chat.PRIVATE,
)
assert isinstance(chat.active_usernames, tuple)
assert chat.active_usernames == ()
def test_to_dict(self, chat):
chat_dict = chat.to_dict()
@ -300,15 +321,25 @@ class TestChatWithoutRequest(TestChatBase):
assert chat_dict["unrestrict_boost_count"] == chat.unrestrict_boost_count
assert chat_dict["birthdate"] == chat.birthdate.to_dict()
assert chat_dict["personal_chat"] == chat.personal_chat.to_dict()
assert chat_dict["first_name"] == chat.first_name
assert chat_dict["last_name"] == chat.last_name
def test_always_tuples_attributes(self):
chat = Chat(
id=123,
title="title",
type=Chat.PRIVATE,
)
assert isinstance(chat.active_usernames, tuple)
assert chat.active_usernames == ()
def test_deprecated_attributes(self, chat):
for depr_attr in _deprecated_attrs:
with pytest.warns(PTBDeprecationWarning, match="deprecated and will only be accessib"):
getattr(chat, depr_attr)
with warnings.catch_warnings(): # No warning should be raised
warnings.simplefilter("error")
chat.id
chat.first_name
def test_deprecated_arguments(self):
for depr_attr in _deprecated_attrs:
with pytest.warns(PTBDeprecationWarning, match="deprecated and will only be availabl"):
Chat(1, "type", **{depr_attr: "1"})
with warnings.catch_warnings(): # No warning should be raised
warnings.simplefilter("error")
Chat(1, "type", first_name="first_name")
def test_enum_init(self):
chat = Chat(id=1, type="foo")
@ -348,10 +379,7 @@ class TestChatWithoutRequest(TestChatBase):
assert chat.full_name == "first\u2022name last\u2022name"
chat = Chat(id=1, type=Chat.PRIVATE, first_name="first\u2022name")
assert chat.full_name == "first\u2022name"
chat = Chat(
id=1,
type=Chat.PRIVATE,
)
chat = Chat(id=1, type=Chat.PRIVATE)
assert chat.full_name is None
def test_effective_name(self):
@ -588,7 +616,7 @@ class TestChatWithoutRequest(TestChatBase):
async def test_set_permissions(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == chat.id
permissions = kwargs["permissions"] == self.permissions
permissions = kwargs["permissions"] == ChatPermissions.all_permissions()
return chat_id and permissions
assert check_shortcut_signature(
@ -600,7 +628,7 @@ class TestChatWithoutRequest(TestChatBase):
assert await check_defaults_handling(chat.set_permissions, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "set_chat_permissions", make_assertion)
assert await chat.set_permissions(permissions=self.permissions)
assert await chat.set_permissions(permissions=ChatPermissions.all_permissions())
async def test_set_administrator_custom_title(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):

View file

@ -0,0 +1,361 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import inspect
from copy import deepcopy
from typing import Union
import pytest
from telegram import (
BackgroundFill,
BackgroundFillFreeformGradient,
BackgroundFillGradient,
BackgroundFillSolid,
BackgroundType,
BackgroundTypeChatTheme,
BackgroundTypeFill,
BackgroundTypePattern,
BackgroundTypeWallpaper,
Dice,
Document,
)
from tests.auxil.slots import mro_slots
ignored = ["self", "api_kwargs"]
class BFDefaults:
color = 0
top_color = 1
bottom_color = 2
rotation_angle = 45
colors = [0, 1, 2]
def background_fill_solid():
return BackgroundFillSolid(BFDefaults.color)
def background_fill_gradient():
return BackgroundFillGradient(
BFDefaults.top_color, BFDefaults.bottom_color, BFDefaults.rotation_angle
)
def background_fill_freeform_gradient():
return BackgroundFillFreeformGradient(BFDefaults.colors)
class BTDefaults:
document = Document(1, 2)
fill = BackgroundFillSolid(color=0)
dark_theme_dimming = 20
is_blurred = True
is_moving = False
intensity = 90
is_inverted = False
theme_name = "ice"
def background_type_fill():
return BackgroundTypeFill(BTDefaults.fill, BTDefaults.dark_theme_dimming)
def background_type_wallpaper():
return BackgroundTypeWallpaper(
BTDefaults.document,
BTDefaults.dark_theme_dimming,
BTDefaults.is_blurred,
BTDefaults.is_moving,
)
def background_type_pattern():
return BackgroundTypePattern(
BTDefaults.document,
BTDefaults.fill,
BTDefaults.intensity,
BTDefaults.is_inverted,
BTDefaults.is_moving,
)
def background_type_chat_theme():
return BackgroundTypeChatTheme(BTDefaults.theme_name)
def make_json_dict(
instance: Union[BackgroundType, BackgroundFill], include_optional_args: bool = False
) -> dict:
"""Used to make the json dict which we use for testing de_json. Similar to iter_args()"""
json_dict = {"type": instance.type}
sig = inspect.signature(instance.__class__.__init__)
for param in sig.parameters.values():
if param.name in ignored: # ignore irrelevant params
continue
val = getattr(instance, param.name)
# Compulsory args-
if param.default is inspect.Parameter.empty:
if hasattr(val, "to_dict"): # convert the user object or any future ones to dict.
val = val.to_dict()
json_dict[param.name] = val
# If we want to test all args (for de_json)-
elif param.default is not inspect.Parameter.empty and include_optional_args:
json_dict[param.name] = val
return json_dict
def iter_args(
instance: Union[BackgroundType, BackgroundFill],
de_json_inst: Union[BackgroundType, BackgroundFill],
include_optional: bool = False,
):
"""
We accept both the regular instance and de_json created instance and iterate over them for
easy one line testing later one.
"""
yield instance.type, de_json_inst.type # yield this here cause it's not available in sig.
sig = inspect.signature(instance.__class__.__init__)
for param in sig.parameters.values():
if param.name in ignored:
continue
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
if (
param.default is not inspect.Parameter.empty and include_optional
) or param.default is inspect.Parameter.empty:
yield inst_at, json_at
@pytest.fixture()
def background_type(request):
return request.param()
@pytest.mark.parametrize(
"background_type",
[
background_type_fill,
background_type_wallpaper,
background_type_pattern,
background_type_chat_theme,
],
indirect=True,
)
class TestBackgroundTypeWithoutRequest:
def test_slot_behaviour(self, background_type):
inst = background_type
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json_required_args(self, bot, background_type):
cls = background_type.__class__
assert cls.de_json({}, bot) is None
json_dict = make_json_dict(background_type)
const_background_type = BackgroundType.de_json(json_dict, bot)
assert const_background_type.api_kwargs == {}
assert isinstance(const_background_type, BackgroundType)
assert isinstance(const_background_type, cls)
for bg_type_at, const_bg_type_at in iter_args(background_type, const_background_type):
assert bg_type_at == const_bg_type_at
def test_de_json_all_args(self, bot, background_type):
json_dict = make_json_dict(background_type, include_optional_args=True)
const_background_type = BackgroundType.de_json(json_dict, bot)
assert const_background_type.api_kwargs == {}
assert isinstance(const_background_type, BackgroundType)
assert isinstance(const_background_type, background_type.__class__)
for bg_type_at, const_bg_type_at in iter_args(
background_type, const_background_type, True
):
assert bg_type_at == const_bg_type_at
def test_de_json_invalid_type(self, background_type, bot):
json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name}
background_type = BackgroundType.de_json(json_dict, bot)
assert type(background_type) is BackgroundType
assert background_type.type == "invalid"
def test_de_json_subclass(self, background_type, bot, chat_id):
"""This makes sure that e.g. BackgroundTypeFill(data, bot) never returns a
BackgroundTypeWallpaper instance."""
cls = background_type.__class__
json_dict = make_json_dict(background_type, True)
assert type(cls.de_json(json_dict, bot)) is cls
def test_to_dict(self, background_type):
bg_type_dict = background_type.to_dict()
assert isinstance(bg_type_dict, dict)
assert bg_type_dict["type"] == background_type.type
for slot in background_type.__slots__: # additional verification for the optional args
if slot in ("fill", "document"):
assert (getattr(background_type, slot)).to_dict() == bg_type_dict[slot]
continue
assert getattr(background_type, slot) == bg_type_dict[slot]
def test_equality(self, background_type):
a = BackgroundType(type="type")
b = BackgroundType(type="type")
c = background_type
d = deepcopy(background_type)
e = Dice(4, "emoji")
sig = inspect.signature(background_type.__class__.__init__)
params = [
"random" for param in sig.parameters.values() if param.name not in [*ignored, "type"]
]
f = background_type.__class__(*params)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
assert c == d
assert hash(c) == hash(d)
assert c != e
assert hash(c) != hash(e)
assert f != c
assert hash(f) != hash(c)
@pytest.fixture()
def background_fill(request):
return request.param()
@pytest.mark.parametrize(
"background_fill",
[
background_fill_solid,
background_fill_gradient,
background_fill_freeform_gradient,
],
indirect=True,
)
class TestBackgroundFillWithoutRequest:
def test_slot_behaviour(self, background_fill):
inst = background_fill
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json_required_args(self, bot, background_fill):
cls = background_fill.__class__
assert cls.de_json({}, bot) is None
json_dict = make_json_dict(background_fill)
const_background_fill = BackgroundFill.de_json(json_dict, bot)
assert const_background_fill.api_kwargs == {}
assert isinstance(const_background_fill, BackgroundFill)
assert isinstance(const_background_fill, cls)
for bg_fill_at, const_bg_fill_at in iter_args(background_fill, const_background_fill):
assert bg_fill_at == const_bg_fill_at
def test_de_json_all_args(self, bot, background_fill):
json_dict = make_json_dict(background_fill, include_optional_args=True)
const_background_fill = BackgroundFill.de_json(json_dict, bot)
assert const_background_fill.api_kwargs == {}
assert isinstance(const_background_fill, BackgroundFill)
assert isinstance(const_background_fill, background_fill.__class__)
for bg_fill_at, const_bg_fill_at in iter_args(
background_fill, const_background_fill, True
):
assert bg_fill_at == const_bg_fill_at
def test_de_json_invalid_type(self, background_fill, bot):
json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name}
background_fill = BackgroundFill.de_json(json_dict, bot)
assert type(background_fill) is BackgroundFill
assert background_fill.type == "invalid"
def test_de_json_subclass(self, background_fill, bot):
"""This makes sure that e.g. BackgroundFillSolid(data, bot) never returns a
BackgroundFillGradient instance."""
cls = background_fill.__class__
json_dict = make_json_dict(background_fill, True)
assert type(cls.de_json(json_dict, bot)) is cls
def test_to_dict(self, background_fill):
bg_fill_dict = background_fill.to_dict()
assert isinstance(bg_fill_dict, dict)
assert bg_fill_dict["type"] == background_fill.type
for slot in background_fill.__slots__: # additional verification for the optional args
if slot == "colors":
assert getattr(background_fill, slot) == tuple(bg_fill_dict[slot])
continue
assert getattr(background_fill, slot) == bg_fill_dict[slot]
def test_equality(self, background_fill):
a = BackgroundFill(type="type")
b = BackgroundFill(type="type")
c = background_fill
d = deepcopy(background_fill)
e = Dice(4, "emoji")
sig = inspect.signature(background_fill.__class__.__init__)
params = [
"random" for param in sig.parameters.values() if param.name not in [*ignored, "type"]
]
f = background_fill.__class__(*params)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
assert c == d
assert hash(c) == hash(d)
assert c != e
assert hash(c) != hash(e)
assert f != c
assert hash(f) != hash(c)

209
tests/test_chatfullinfo.py Normal file
View file

@ -0,0 +1,209 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import datetime
import warnings
import pytest
from telegram import (
Birthdate,
BusinessIntro,
BusinessLocation,
BusinessOpeningHours,
BusinessOpeningHoursInterval,
Chat,
ChatFullInfo,
ChatLocation,
ChatPermissions,
Location,
ReactionTypeCustomEmoji,
ReactionTypeEmoji,
)
from telegram._chat import _deprecated_attrs
from telegram._utils.datetime import UTC, to_timestamp
from telegram.constants import ReactionEmoji
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def chat_full_info(bot):
chat = ChatFullInfo(
TestChatInfoBase.id_,
type=TestChatInfoBase.type_,
accent_color_id=TestChatInfoBase.accent_color_id,
max_reaction_count=TestChatInfoBase.max_reaction_count,
title=TestChatInfoBase.title,
username=TestChatInfoBase.username,
sticker_set_name=TestChatInfoBase.sticker_set_name,
can_set_sticker_set=TestChatInfoBase.can_set_sticker_set,
permissions=TestChatInfoBase.permissions,
slow_mode_delay=TestChatInfoBase.slow_mode_delay,
bio=TestChatInfoBase.bio,
linked_chat_id=TestChatInfoBase.linked_chat_id,
location=TestChatInfoBase.location,
has_private_forwards=True,
has_protected_content=True,
has_visible_history=True,
join_to_send_messages=True,
join_by_request=True,
has_restricted_voice_and_video_messages=True,
is_forum=True,
active_usernames=TestChatInfoBase.active_usernames,
emoji_status_custom_emoji_id=TestChatInfoBase.emoji_status_custom_emoji_id,
emoji_status_expiration_date=TestChatInfoBase.emoji_status_expiration_date,
has_aggressive_anti_spam_enabled=TestChatInfoBase.has_aggressive_anti_spam_enabled,
has_hidden_members=TestChatInfoBase.has_hidden_members,
available_reactions=TestChatInfoBase.available_reactions,
background_custom_emoji_id=TestChatInfoBase.background_custom_emoji_id,
profile_accent_color_id=TestChatInfoBase.profile_accent_color_id,
profile_background_custom_emoji_id=TestChatInfoBase.profile_background_custom_emoji_id,
unrestrict_boost_count=TestChatInfoBase.unrestrict_boost_count,
custom_emoji_sticker_set_name=TestChatInfoBase.custom_emoji_sticker_set_name,
business_intro=TestChatInfoBase.business_intro,
business_location=TestChatInfoBase.business_location,
business_opening_hours=TestChatInfoBase.business_opening_hours,
birthdate=Birthdate(1, 1),
personal_chat=TestChatInfoBase.personal_chat,
)
chat.set_bot(bot)
chat._unfreeze()
return chat
class TestChatInfoBase:
id_ = -28767330
max_reaction_count = 2
title = "ToledosPalaceBot - Group"
type_ = "group"
username = "username"
all_members_are_administrators = False
sticker_set_name = "stickers"
can_set_sticker_set = False
permissions = ChatPermissions(
can_send_messages=True,
can_change_info=False,
can_invite_users=True,
)
slow_mode_delay = 30
bio = "I'm a Barbie Girl in a Barbie World"
linked_chat_id = 11880
location = ChatLocation(Location(123, 456), "Barbie World")
has_protected_content = True
has_visible_history = True
has_private_forwards = True
join_to_send_messages = True
join_by_request = True
has_restricted_voice_and_video_messages = True
is_forum = True
active_usernames = ["These", "Are", "Usernames!"]
emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID"
emoji_status_expiration_date = datetime.datetime.now(tz=UTC).replace(microsecond=0)
has_aggressive_anti_spam_enabled = True
has_hidden_members = True
available_reactions = [
ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN),
ReactionTypeCustomEmoji("custom_emoji_id"),
]
business_intro = BusinessIntro("Title", "Description", None)
business_location = BusinessLocation("Address", Location(123, 456))
business_opening_hours = BusinessOpeningHours(
"Country/City",
[BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60)],
)
accent_color_id = 1
background_custom_emoji_id = "background_custom_emoji_id"
profile_accent_color_id = 2
profile_background_custom_emoji_id = "profile_background_custom_emoji_id"
unrestrict_boost_count = 100
custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name"
birthdate = Birthdate(1, 1)
personal_chat = Chat(3, "private", "private")
class TestChatWithoutRequest(TestChatInfoBase):
def test_slot_behaviour(self, chat_full_info):
cfi = chat_full_info
for attr in cfi.__slots__:
assert getattr(cfi, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(cfi)) == len(set(mro_slots(cfi))), "duplicate slot"
def test_de_json(self, bot):
json_dict = {
"id": self.id_,
"title": self.title,
"type": self.type_,
"accent_color_id": self.accent_color_id,
"max_reaction_count": self.max_reaction_count,
"username": self.username,
"all_members_are_administrators": self.all_members_are_administrators,
"sticker_set_name": self.sticker_set_name,
"can_set_sticker_set": self.can_set_sticker_set,
"permissions": self.permissions.to_dict(),
"slow_mode_delay": self.slow_mode_delay,
"bio": self.bio,
"business_intro": self.business_intro.to_dict(),
"business_location": self.business_location.to_dict(),
"business_opening_hours": self.business_opening_hours.to_dict(),
"has_protected_content": self.has_protected_content,
"has_visible_history": self.has_visible_history,
"has_private_forwards": self.has_private_forwards,
"linked_chat_id": self.linked_chat_id,
"location": self.location.to_dict(),
"join_to_send_messages": self.join_to_send_messages,
"join_by_request": self.join_by_request,
"has_restricted_voice_and_video_messages": (
self.has_restricted_voice_and_video_messages
),
"is_forum": self.is_forum,
"active_usernames": self.active_usernames,
"emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id,
"emoji_status_expiration_date": to_timestamp(self.emoji_status_expiration_date),
"has_aggressive_anti_spam_enabled": self.has_aggressive_anti_spam_enabled,
"has_hidden_members": self.has_hidden_members,
"available_reactions": [reaction.to_dict() for reaction in self.available_reactions],
"background_custom_emoji_id": self.background_custom_emoji_id,
"profile_accent_color_id": self.profile_accent_color_id,
"profile_background_custom_emoji_id": self.profile_background_custom_emoji_id,
"unrestrict_boost_count": self.unrestrict_boost_count,
"custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name,
"birthdate": self.birthdate.to_dict(),
"personal_chat": self.personal_chat.to_dict(),
}
cfi = ChatFullInfo.de_json(json_dict, bot)
assert cfi.max_reaction_count == self.max_reaction_count
def test_to_dict(self, chat_full_info):
cfi = chat_full_info
cfi_dict = cfi.to_dict()
assert isinstance(cfi_dict, dict)
assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count
def test_attr_access_no_warning(self, chat_full_info):
cfi = chat_full_info
for depr_attr in _deprecated_attrs:
with warnings.catch_warnings(): # No warning should be raised
warnings.simplefilter("error")
getattr(cfi, depr_attr)
def test_cfi_creation_no_warning(self, chat_full_info):
cfi = chat_full_info
with warnings.catch_warnings():
dict = cfi.to_dict()
ChatFullInfo(**dict)

View file

@ -82,7 +82,9 @@ def invite_link(user):
@pytest.fixture(scope="module")
def chat_member_updated(user, chat, old_chat_member, new_chat_member, invite_link, time):
return ChatMemberUpdated(chat, user, time, old_chat_member, new_chat_member, invite_link, True)
return ChatMemberUpdated(
chat, user, time, old_chat_member, new_chat_member, invite_link, True, True
)
class TestChatMemberUpdatedBase:
@ -129,6 +131,7 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase):
"new_chat_member": new_chat_member.to_dict(),
"invite_link": invite_link.to_dict(),
"via_chat_folder_invite_link": True,
"via_join_request": True,
}
chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot)
@ -142,6 +145,7 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase):
assert chat_member_updated.new_chat_member == new_chat_member
assert chat_member_updated.invite_link == invite_link
assert chat_member_updated.via_chat_folder_invite_link is True
assert chat_member_updated.via_join_request is True
def test_de_json_localization(
self, bot, raw_bot, tz_bot, user, chat, old_chat_member, new_chat_member, time, invite_link
@ -188,6 +192,7 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase):
chat_member_updated_dict["via_chat_folder_invite_link"]
== chat_member_updated.via_chat_folder_invite_link
)
assert chat_member_updated_dict["via_join_request"] == chat_member_updated.via_join_request
def test_equality(self, time, old_chat_member, new_chat_member, invite_link):
a = ChatMemberUpdated(

View file

@ -27,7 +27,10 @@ exclude_dirs = {
/ "_passport",
}
exclude_patterns = {re.compile(re.escape("self.type: ReactionType = type"))}
exclude_patterns = {
re.compile(re.escape("self.type: ReactionType = type")),
re.compile(re.escape("self.type: BackgroundType = type")),
}
def test_types_are_converted_to_enum():

View file

@ -24,8 +24,10 @@ import pytest
from telegram import (
Animation,
Audio,
BackgroundTypeChatTheme,
Bot,
Chat,
ChatBackground,
ChatBoostAdded,
ChatShared,
Contact,
@ -270,6 +272,7 @@ def message(bot):
{"is_from_offline": True},
{"sender_business_bot": User(1, "BusinessBot", True)},
{"business_connection_id": "123456789"},
{"chat_background_set": ChatBackground(type=BackgroundTypeChatTheme("ice"))},
],
ids=[
"reply",
@ -338,6 +341,7 @@ def message(bot):
"sender_business_bot",
"business_connection_id",
"is_from_offline",
"chat_background_set",
],
)
def message_params(bot, request):
@ -2414,7 +2418,8 @@ class TestMessageWithoutRequest(TestMessageBase):
message_id = kwargs["message_id"] == message.message_id
latitude = kwargs["latitude"] == 1
longitude = kwargs["longitude"] == 2
return chat_id and message_id and longitude and latitude
live = kwargs["live_period"] == 900
return chat_id and message_id and longitude and latitude and live
assert check_shortcut_signature(
Message.edit_live_location,
@ -2432,7 +2437,7 @@ class TestMessageWithoutRequest(TestMessageBase):
assert await check_defaults_handling(message.edit_live_location, message.get_bot())
monkeypatch.setattr(message.get_bot(), "edit_message_live_location", make_assertion)
assert await message.edit_live_location(latitude=1, longitude=2)
assert await message.edit_live_location(latitude=1, longitude=2, live_period=900)
async def test_stop_live_location(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):

View file

@ -148,8 +148,11 @@ def check_param_type(
# Now let's do the checking, starting with "Array of ..." types.
if "Array of " in tg_param_type:
# For exceptions just check if they contain the annotation
if ptb_param.name in PTCE.ARRAY_OF_EXCEPTIONS:
return PTCE.ARRAY_OF_EXCEPTIONS[ptb_param.name] in str(ptb_annotation), Sequence
if any(ptb_param.name in key for key in PTCE.ARRAY_OF_EXCEPTIONS):
for (p_name, is_expected_class), exception_type in PTCE.ARRAY_OF_EXCEPTIONS.items():
if ptb_param.name == p_name and is_class is is_expected_class:
log("Checking that `%s` is an exception!\n", ptb_param.name)
return exception_type in str(ptb_annotation), Sequence
obj_match: re.Match | None = re.search(ARRAY_OF_PATTERN, tg_param_type)
if obj_match is None:

View file

@ -20,6 +20,7 @@
from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice
from telegram._chat import _deprecated_attrs
from tests.test_official.helpers import _get_params_base
IGNORED_OBJECTS = ("ResponseParameters",)
@ -47,15 +48,17 @@ class ParamTypeCheckingExceptions:
"sticker": Sticker,
}
# TODO: Look into merging this with COMPLEX_TYPES
# Exceptions to the "Array of" types, where we accept more types than the official API
# key: parameter name, value: type which must be present in the annotation
# key: (parameter name, is_class), value: type which must be present in the annotation
ARRAY_OF_EXCEPTIONS = {
"results": "InlineQueryResult", # + Callable
"commands": "BotCommand", # + tuple[str, str]
"keyboard": "KeyboardButton", # + sequence[sequence[str]]
"reaction": "ReactionType", # + str
("results", False): "InlineQueryResult", # + Callable
("commands", False): "BotCommand", # + tuple[str, str]
("keyboard", True): "KeyboardButton", # + sequence[sequence[str]]
("reaction", False): "ReactionType", # + str
("options", False): "InputPollOption", # + str
# TODO: Deprecated and will be corrected (and removed) in next major PTB version:
"file_hashes": "List[str]",
("file_hashes", True): "List[str]",
}
# Special cases for other parameters that accept more types than the official API, and are
@ -120,6 +123,8 @@ PTB_EXTRA_PARAMS = {
"ChatBoostSource": {"source"}, # attributes common to all subclasses
"MessageOrigin": {"type", "date"}, # attributes common to all subclasses
"ReactionType": {"type"}, # attributes common to all subclasses
"BackgroundType": {"type"}, # attributes common to all subclasses
"BackgroundFill": {"type"}, # attributes common to all subclasses
"InputTextMessageContent": {"disable_web_page_preview"}, # convenience arg, here for bw compat
}
@ -143,6 +148,8 @@ PTB_IGNORED_PARAMS = {
r"MessageOrigin\w+": {"type"},
r"ChatBoostSource\w+": {"source"},
r"ReactionType\w+": {"type"},
r"BackgroundType\w+": {"type"},
r"BackgroundFill\w+": {"type"},
}
@ -166,7 +173,9 @@ def ignored_param_requirements(object_name: str) -> set[str]:
# Arguments that are optional arguments for now for backwards compatibility
BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {}
BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {
"Chat": set(_deprecated_attrs), # removed by bot api 7.3
}
def backwards_compat_kwargs(object_name: str) -> set[str]:

View file

@ -19,15 +19,97 @@ from datetime import datetime, timedelta, timezone
import pytest
from telegram import Chat, MessageEntity, Poll, PollAnswer, PollOption, User
from telegram import Chat, InputPollOption, MessageEntity, Poll, PollAnswer, PollOption, User
from telegram._utils.datetime import UTC, to_timestamp
from telegram.constants import PollType
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def input_poll_option():
out = InputPollOption(
text=TestInputPollOptionBase.text,
text_parse_mode=TestInputPollOptionBase.text_parse_mode,
text_entities=TestInputPollOptionBase.text_entities,
)
out._unfreeze()
return out
class TestInputPollOptionBase:
text = "test option"
text_parse_mode = "MarkdownV2"
text_entities = [
MessageEntity(0, 4, MessageEntity.BOLD),
MessageEntity(5, 7, MessageEntity.ITALIC),
]
class TestInputPollOptionWithoutRequest(TestInputPollOptionBase):
def test_slot_behaviour(self, input_poll_option):
for attr in input_poll_option.__slots__:
assert getattr(input_poll_option, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(input_poll_option)) == len(
set(mro_slots(input_poll_option))
), "duplicate slot"
def test_de_json(self):
assert InputPollOption.de_json({}, None) is None
json_dict = {
"text": self.text,
"text_parse_mode": self.text_parse_mode,
"text_entities": [e.to_dict() for e in self.text_entities],
}
input_poll_option = InputPollOption.de_json(json_dict, None)
assert input_poll_option.api_kwargs == {}
assert input_poll_option.text == self.text
assert input_poll_option.text_parse_mode == self.text_parse_mode
assert input_poll_option.text_entities == tuple(self.text_entities)
def test_to_dict(self, input_poll_option):
input_poll_option_dict = input_poll_option.to_dict()
assert isinstance(input_poll_option_dict, dict)
assert input_poll_option_dict["text"] == input_poll_option.text
assert input_poll_option_dict["text_parse_mode"] == input_poll_option.text_parse_mode
assert input_poll_option_dict["text_entities"] == [
e.to_dict() for e in input_poll_option.text_entities
]
# Test that the default-value parameter is handled correctly
input_poll_option = InputPollOption("text")
input_poll_option_dict = input_poll_option.to_dict()
assert "text_parse_mode" not in input_poll_option_dict
def test_equality(self):
a = InputPollOption("text")
b = InputPollOption("text", self.text_parse_mode)
c = InputPollOption("text", text_entities=self.text_entities)
d = InputPollOption("different_text")
e = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True)
assert a == b
assert hash(a) == hash(b)
assert a == c
assert hash(a) == hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
@pytest.fixture(scope="module")
def poll_option():
out = PollOption(text=TestPollOptionBase.text, voter_count=TestPollOptionBase.voter_count)
out = PollOption(
text=TestPollOptionBase.text,
voter_count=TestPollOptionBase.voter_count,
text_entities=TestPollOptionBase.text_entities,
)
out._unfreeze()
return out
@ -35,6 +117,10 @@ def poll_option():
class TestPollOptionBase:
text = "test option"
voter_count = 3
text_entities = [
MessageEntity(MessageEntity.BOLD, 0, 4),
MessageEntity(MessageEntity.ITALIC, 5, 6),
]
class TestPollOptionWithoutRequest(TestPollOptionBase):
@ -51,12 +137,43 @@ class TestPollOptionWithoutRequest(TestPollOptionBase):
assert poll_option.text == self.text
assert poll_option.voter_count == self.voter_count
def test_de_json_all(self):
json_dict = {
"text": self.text,
"voter_count": self.voter_count,
"text_entities": [e.to_dict() for e in self.text_entities],
}
poll_option = PollOption.de_json(json_dict, None)
assert PollOption.de_json(None, None) is None
assert poll_option.api_kwargs == {}
assert poll_option.text == self.text
assert poll_option.voter_count == self.voter_count
assert poll_option.text_entities == tuple(self.text_entities)
def test_to_dict(self, poll_option):
poll_option_dict = poll_option.to_dict()
assert isinstance(poll_option_dict, dict)
assert poll_option_dict["text"] == poll_option.text
assert poll_option_dict["voter_count"] == poll_option.voter_count
assert poll_option_dict["text_entities"] == [
e.to_dict() for e in poll_option.text_entities
]
def test_parse_entity(self, poll_option):
entity = MessageEntity(MessageEntity.BOLD, 0, 4)
poll_option.text_entities = [entity]
assert poll_option.parse_entity(entity) == "test"
def test_parse_entities(self, poll_option):
entity = MessageEntity(MessageEntity.BOLD, 0, 4)
entity_2 = MessageEntity(MessageEntity.ITALIC, 5, 6)
poll_option.text_entities = [entity, entity_2]
assert poll_option.parse_entities(MessageEntity.BOLD) == {entity: "test"}
assert poll_option.parse_entities() == {entity: "test", entity_2: "option"}
def test_equality(self):
a = PollOption("text", 1)
@ -159,6 +276,7 @@ def poll():
explanation_entities=TestPollBase.explanation_entities,
open_period=TestPollBase.open_period,
close_date=TestPollBase.close_date,
question_entities=TestPollBase.question_entities,
)
poll._unfreeze()
return poll
@ -166,7 +284,7 @@ def poll():
class TestPollBase:
id_ = "id"
question = "Test?"
question = "Test Question?"
options = [PollOption("test", 10), PollOption("test2", 11)]
total_voter_count = 0
is_closed = True
@ -180,6 +298,10 @@ class TestPollBase:
explanation_entities = [MessageEntity(13, 17, MessageEntity.URL)]
open_period = 42
close_date = datetime.now(timezone.utc)
question_entities = [
MessageEntity(MessageEntity.BOLD, 0, 4),
MessageEntity(MessageEntity.ITALIC, 5, 8),
]
class TestPollWithoutRequest(TestPollBase):
@ -197,6 +319,7 @@ class TestPollWithoutRequest(TestPollBase):
"explanation_entities": [self.explanation_entities[0].to_dict()],
"open_period": self.open_period,
"close_date": to_timestamp(self.close_date),
"question_entities": [e.to_dict() for e in self.question_entities],
}
poll = Poll.de_json(json_dict, bot)
assert poll.api_kwargs == {}
@ -218,6 +341,7 @@ class TestPollWithoutRequest(TestPollBase):
assert poll.open_period == self.open_period
assert abs(poll.close_date - self.close_date) < timedelta(seconds=1)
assert to_timestamp(poll.close_date) == to_timestamp(self.close_date)
assert poll.question_entities == tuple(self.question_entities)
def test_de_json_localization(self, tz_bot, bot, raw_bot):
json_dict = {
@ -233,6 +357,7 @@ class TestPollWithoutRequest(TestPollBase):
"explanation_entities": [self.explanation_entities[0].to_dict()],
"open_period": self.open_period,
"close_date": to_timestamp(self.close_date),
"question_entities": [e.to_dict() for e in self.question_entities],
}
poll_raw = Poll.de_json(json_dict, raw_bot)
@ -265,6 +390,7 @@ class TestPollWithoutRequest(TestPollBase):
assert poll_dict["explanation_entities"] == [poll.explanation_entities[0].to_dict()]
assert poll_dict["open_period"] == poll.open_period
assert poll_dict["close_date"] == to_timestamp(poll.close_date)
assert poll_dict["question_entities"] == [e.to_dict() for e in poll.question_entities]
def test_equality(self):
a = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True)
@ -305,7 +431,7 @@ class TestPollWithoutRequest(TestPollBase):
)
assert poll.type is PollType.QUIZ
def test_parse_entity(self, poll):
def test_parse_explanation_entity(self, poll):
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
poll.explanation_entities = [entity]
@ -323,10 +449,36 @@ class TestPollWithoutRequest(TestPollBase):
allows_multiple_answers=False,
).parse_explanation_entity(entity)
def test_parse_entities(self, poll):
def test_parse_explanation_entities(self, poll):
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1)
poll.explanation_entities = [entity_2, entity]
assert poll.parse_explanation_entities(MessageEntity.URL) == {entity: "http://google.com"}
assert poll.parse_explanation_entities() == {entity: "http://google.com", entity_2: "h"}
with pytest.raises(RuntimeError, match="Poll has no"):
Poll(
"id",
"question",
[PollOption("text", voter_count=0)],
total_voter_count=0,
is_closed=False,
is_anonymous=False,
type=Poll.QUIZ,
allows_multiple_answers=False,
).parse_explanation_entities()
def test_parse_question_entity(self, poll):
entity = MessageEntity(MessageEntity.ITALIC, 5, 8)
poll.question_entities = [entity]
assert poll.parse_question_entity(entity) == "Question"
def test_parse_question_entities(self, poll):
entity = MessageEntity(MessageEntity.ITALIC, 5, 8)
entity_2 = MessageEntity(MessageEntity.BOLD, 0, 4)
poll.question_entities = [entity_2, entity]
assert poll.parse_question_entities(MessageEntity.ITALIC) == {entity: "Question"}
assert poll.parse_question_entities() == {entity: "Question", entity_2: "Test"}

View file

@ -33,7 +33,7 @@ class TestWarnings:
[
(PTBUserWarning("test message")),
(PTBRuntimeWarning("test message")),
(PTBDeprecationWarning()),
(PTBDeprecationWarning("20.6", "test message")),
],
)
def test_slots_behavior(self, inst):
@ -80,9 +80,8 @@ class TestWarnings:
assert str(recwarn[1].message) == "test message 2"
assert Path(recwarn[1].filename) == expected_file, "incorrect stacklevel!"
warn("test message 3", stacklevel=1, category=PTBDeprecationWarning)
expected_file = Path(__file__)
warn(PTBDeprecationWarning("20.6", "test message 3"), stacklevel=1)
assert len(recwarn) == 3
assert recwarn[2].category is PTBDeprecationWarning
assert str(recwarn[2].message) == "test message 3"
assert Path(recwarn[2].filename) == expected_file, "incorrect stacklevel!"
assert str(recwarn[2].message) == "Deprecated since version 20.6: test message 3"
assert Path(recwarn[2].filename) == Path(__file__), "incorrect stacklevel!"