From 661045f962e427b795229dbe87a14151ca8f5596 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:02:16 +0200 Subject: [PATCH 01/11] Update API Token for Local Testing Bot (#4837) --- changes/unreleased/4837.cVTsY7pxfQqgVXfFW9hgZK.toml | 6 ++++++ tests/auxil/ci_bots.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4837.cVTsY7pxfQqgVXfFW9hgZK.toml diff --git a/changes/unreleased/4837.cVTsY7pxfQqgVXfFW9hgZK.toml b/changes/unreleased/4837.cVTsY7pxfQqgVXfFW9hgZK.toml new file mode 100644 index 00000000000..799b32d75fe --- /dev/null +++ b/changes/unreleased/4837.cVTsY7pxfQqgVXfFW9hgZK.toml @@ -0,0 +1,6 @@ +internal = "Update API Token for Local Testing Bot" + +[[pull_requests]] +uid = "4837" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/tests/auxil/ci_bots.py b/tests/auxil/ci_bots.py index 81e0c4819b8..cb05004a0d0 100644 --- a/tests/auxil/ci_bots.py +++ b/tests/auxil/ci_bots.py @@ -30,7 +30,7 @@ # These bots are only able to talk in our test chats, so they are quite useless for other # purposes than testing. FALLBACKS = ( - "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" + "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRmdqXzhmNFVlV1hrb3VVUnpUZThhRUY0UGNFQkRxdlY0IiwgInBheW1lbnRfc" "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj" "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzODA" "wNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgIm5hbWUiOiAiUFRCIHRlc3RzIGZh" From f55a4c24b64f1576a9d1d53ac7fedec778f5bcb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:24:02 +0200 Subject: [PATCH 02/11] Bump `stefanzweifel/git-auto-commit-action` from 5.2.0 to 6.0.1 (#4840) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/chango.yml | 2 +- changes/unreleased/4840.jz9uGugc5DUd8x8pQHPyzg.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4840.jz9uGugc5DUd8x8pQHPyzg.toml diff --git a/.github/workflows/chango.yml b/.github/workflows/chango.yml index 1b3dfe0caa8..cba4fb7d7b3 100644 --- a/.github/workflows/chango.yml +++ b/.github/workflows/chango.yml @@ -60,7 +60,7 @@ jobs: - name: Commit & Push if: steps.check_title.outputs.IS_RELEASE_PR == 'true' - uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0 + uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1 with: commit_message: "Do chango Release" repository: ./target-repo diff --git a/changes/unreleased/4840.jz9uGugc5DUd8x8pQHPyzg.toml b/changes/unreleased/4840.jz9uGugc5DUd8x8pQHPyzg.toml new file mode 100644 index 00000000000..bf51748b985 --- /dev/null +++ b/changes/unreleased/4840.jz9uGugc5DUd8x8pQHPyzg.toml @@ -0,0 +1,5 @@ +internal = "Bump stefanzweifel/git-auto-commit-action from 5.2.0 to 6.0.1" +[[pull_requests]] +uid = "4840" +author_uid = "dependabot" +closes_threads = [] From f1d4264f6898dd0a641e1162973b6881277a7a8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:24:19 +0200 Subject: [PATCH 03/11] Bump `github/codeql-action` from 3.28.18 to 3.29.2 (#4841) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/gha_security.yml | 2 +- changes/unreleased/4841.HHVCCXXZbYAaaQVmKHCJm2.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4841.HHVCCXXZbYAaaQVmKHCJm2.toml diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index ff207f3e8b7..8114087d237 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -27,7 +27,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: results.sarif category: zizmor \ No newline at end of file diff --git a/changes/unreleased/4841.HHVCCXXZbYAaaQVmKHCJm2.toml b/changes/unreleased/4841.HHVCCXXZbYAaaQVmKHCJm2.toml new file mode 100644 index 00000000000..5959a63505f --- /dev/null +++ b/changes/unreleased/4841.HHVCCXXZbYAaaQVmKHCJm2.toml @@ -0,0 +1,5 @@ +internal = "Bump github/codeql-action from 3.28.18 to 3.29.2" +[[pull_requests]] +uid = "4841" +author_uid = "dependabot" +closes_threads = [] From ea967b5e711158f7e97a3417bac4a5b805bd9d76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:52:12 +0200 Subject: [PATCH 04/11] Bump `astral-sh/setup-uv` from 5.4.1 to 6.3.1 (#4842) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/gha_security.yml | 2 +- changes/unreleased/4842.PSW9ZbxENhwfRhbSfemCwE.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4842.PSW9ZbxENhwfRhbSfemCwE.toml diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml index 8114087d237..491491547d4 100644 --- a/.github/workflows/gha_security.yml +++ b/.github/workflows/gha_security.yml @@ -21,7 +21,7 @@ jobs: with: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: Run zizmor run: uvx zizmor --persona=pedantic --format sarif . > results.sarif env: diff --git a/changes/unreleased/4842.PSW9ZbxENhwfRhbSfemCwE.toml b/changes/unreleased/4842.PSW9ZbxENhwfRhbSfemCwE.toml new file mode 100644 index 00000000000..25aab233487 --- /dev/null +++ b/changes/unreleased/4842.PSW9ZbxENhwfRhbSfemCwE.toml @@ -0,0 +1,5 @@ +internal = "Bump astral-sh/setup-uv from 5.4.1 to 6.3.1" +[[pull_requests]] +uid = "4842" +author_uid = "dependabot" +closes_threads = [] From 3f5f3a68881765e0278c257136a3bcff9289f9ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:36:55 +0200 Subject: [PATCH 05/11] Bump sigstore/gh-action-sigstore-python from 3.0.0 to 3.0.1 (#4843) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/release_pypi.yml | 2 +- .github/workflows/release_test_pypi.yml | 2 +- changes/unreleased/4843.f5aZ5Vpxevw8fEEudawU5u.toml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changes/unreleased/4843.f5aZ5Vpxevw8fEEudawU5u.toml diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index ec8e822da3b..1b2e2445eab 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -86,7 +86,7 @@ jobs: sha1sum $file > $file.sha1 done - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0 + uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1 with: inputs: >- ./dist/*.tar.gz diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index 9e673517765..7497ffe70fc 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -88,7 +88,7 @@ jobs: sha1sum $file > $file.sha1 done - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0 + uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1 with: inputs: >- ./dist/*.tar.gz diff --git a/changes/unreleased/4843.f5aZ5Vpxevw8fEEudawU5u.toml b/changes/unreleased/4843.f5aZ5Vpxevw8fEEudawU5u.toml new file mode 100644 index 00000000000..6a2593d4fc3 --- /dev/null +++ b/changes/unreleased/4843.f5aZ5Vpxevw8fEEudawU5u.toml @@ -0,0 +1,5 @@ +internal = "Bump sigstore/gh-action-sigstore-python from 3.0.0 to 3.0.1" +[[pull_requests]] +uid = "4843" +author_uid = "dependabot" +closes_threads = [] From cedfc99e246095248d1311a35a103ac2a8832166 Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Sat, 5 Jul 2025 14:05:52 +0300 Subject: [PATCH 06/11] Remove Functionality Deprecated in API 9.0 (#4852) --- .../4852.mz3RDjX636ZdGoR456C6v9.toml | 11 ++++ src/telegram/_business.py | 56 ++---------------- src/telegram/_chatfullinfo.py | 59 ++----------------- .../_payment/stars/transactionpartner.py | 7 +-- src/telegram/constants.py | 57 ++++-------------- .../_payment/stars/test_transactionpartner.py | 8 +-- tests/auxil/dummy_objects.py | 3 +- tests/ext/test_businessconnectionhandler.py | 3 +- tests/test_business_classes.py | 35 ----------- tests/test_business_methods.py | 10 +++- tests/test_chatfullinfo.py | 31 ---------- tests/test_official/exceptions.py | 9 --- tests/test_update.py | 3 +- 13 files changed, 47 insertions(+), 245 deletions(-) create mode 100644 changes/unreleased/4852.mz3RDjX636ZdGoR456C6v9.toml diff --git a/changes/unreleased/4852.mz3RDjX636ZdGoR456C6v9.toml b/changes/unreleased/4852.mz3RDjX636ZdGoR456C6v9.toml new file mode 100644 index 00000000000..73640caded1 --- /dev/null +++ b/changes/unreleased/4852.mz3RDjX636ZdGoR456C6v9.toml @@ -0,0 +1,11 @@ +breaking = """Remove Functionality Deprecated in API 9.0 + +* Remove deprecated argument and attribute ``BusinessConnection.can_reply``. +* Remove deprecated argument and attribute ``ChatFullInfo.can_send_gift`` +* Remove deprecated class ``constants.StarTransactions``. Please instead use :attr:`telegram.constants.Nanostar.VALUE`. +* Remove deprecated attributes ``constants.StarTransactionsLimit.NANOSTAR_MIN_AMOUNT`` and ``constants.StarTransactionsLimit.NANOSTAR_MAX_AMOUNT``. Please instead use :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT` and :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`. +""" +[[pull_requests]] +uid = "4852" +author_uid = "aelkheir" +closes_threads = [] diff --git a/src/telegram/_business.py b/src/telegram/_business.py index dd055426654..e9001d4bdf9 100644 --- a/src/telegram/_business.py +++ b/src/telegram/_business.py @@ -30,12 +30,6 @@ from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict -from telegram._utils.warnings import warn -from telegram._utils.warnings_transition import ( - build_deprecation_warning_message, - warn_about_deprecated_attr_in_property, -) -from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot @@ -195,7 +189,10 @@ class BusinessConnection(TelegramObject): .. versionadded:: 21.1 .. versionchanged:: 22.1 - Equality comparison now considers :attr:`rights` instead of :attr:`can_reply`. + Equality comparison now considers :attr:`rights` instead of ``can_reply``. + + .. versionremoved:: NEXT.VERSION + Removed argument and attribute ``can_reply`` deprecated by API 9.0. Args: id (:obj:`str`): Unique identifier of the business connection. @@ -203,11 +200,6 @@ class BusinessConnection(TelegramObject): user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the business connection. date (:obj:`datetime.datetime`): Date the connection was established in Unix time. - can_reply (:obj:`bool`, optional): True, if the bot can act on behalf of the business - account in chats that were active in the last 24 hours. - - .. deprecated:: 22.1 - Bot API 9.0 deprecated this argument in favor of :paramref:`rights`. is_enabled (:obj:`bool`): True, if the connection is active. rights (:class:`BusinessBotRights`, optional): Rights of the business bot. @@ -226,7 +218,6 @@ class BusinessConnection(TelegramObject): """ __slots__ = ( - "_can_reply", "date", "id", "is_enabled", @@ -241,37 +232,16 @@ def __init__( user: "User", user_chat_id: int, date: dtm.datetime, - can_reply: Optional[bool] = None, - # temporarily optional to account for changed signature - # tags: deprecated 22.1; bot api 9.0 - is_enabled: Optional[bool] = None, + is_enabled: bool, rights: Optional[BusinessBotRights] = None, *, api_kwargs: Optional[JSONDict] = None, ): - if is_enabled is None: - raise TypeError("Missing required argument `is_enabled`") - - if can_reply is not None: - warn( - PTBDeprecationWarning( - version="22.1", - message=build_deprecation_warning_message( - deprecated_name="can_reply", - new_name="rights", - bot_api_version="9.0", - object_type="parameter", - ), - ), - stacklevel=2, - ) - super().__init__(api_kwargs=api_kwargs) self.id: str = id self.user: User = user self.user_chat_id: int = user_chat_id self.date: dtm.datetime = date - self._can_reply: Optional[bool] = can_reply self.is_enabled: bool = is_enabled self.rights: Optional[BusinessBotRights] = rights @@ -286,22 +256,6 @@ def __init__( self._freeze() - @property - def can_reply(self) -> Optional[bool]: - """:obj:`bool`: Optional. True, if the bot can act on behalf of the business account in - chats that were active in the last 24 hours. - - .. deprecated:: 22.1 - Bot API 9.0 deprecated this argument in favor of :attr:`rights` - """ - warn_about_deprecated_attr_in_property( - deprecated_attr_name="can_reply", - new_attr_name="rights", - bot_api_version="9.0", - ptb_version="22.1", - ) - return self._can_reply - @classmethod def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessConnection": """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index 1850429b027..e1abdb65a1e 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -41,12 +41,6 @@ get_timedelta_value, ) from telegram._utils.types import JSONDict, TimePeriod -from telegram._utils.warnings import warn -from telegram._utils.warnings_transition import ( - build_deprecation_warning_message, - warn_about_deprecated_attr_in_property, -) -from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot, BusinessIntro, BusinessLocation, BusinessOpeningHours, Message @@ -66,6 +60,9 @@ class ChatFullInfo(_ChatBase): object. Previously those were only available because this class inherited from :class:`telegram.Chat`. + .. versionremoved:: NEXT.VERSION + Removed argument and attribute ``can_send_gift`` deprecated by API 9.0. + Args: id (:obj:`int`): Unique identifier for this chat. type (:obj:`str`): Type of chat, can be either :attr:`PRIVATE`, :attr:`GROUP`, @@ -226,13 +223,6 @@ class ChatFullInfo(_ChatBase): sent or forwarded to the channel chat. The field is available only for channel chats. .. versionadded:: 21.4 - can_send_gift (:obj:`bool`, optional): :obj:`True`, if gifts can be sent to the chat. - - .. versionadded:: 21.11 - - .. deprecated:: 22.1 - Bot API 9.0 introduced :paramref:`accepted_gift_types`, replacing this argument. - Hence, this argument will be removed in future versions. Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -403,7 +393,6 @@ class ChatFullInfo(_ChatBase): """ __slots__ = ( - "_can_send_gift", "_message_auto_delete_time", "_slow_mode_delay", "accent_color_id", @@ -450,6 +439,7 @@ def __init__( type: str, accent_color_id: int, max_reaction_count: int, + accepted_gift_types: AcceptedGiftTypes, title: Optional[str] = None, username: Optional[str] = None, first_name: Optional[str] = None, @@ -490,10 +480,6 @@ def __init__( linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, can_send_paid_media: Optional[bool] = None, - # tags: deprecated 22.1; bot api 9.0 - can_send_gift: Optional[bool] = None, - # temporarily optional to account for changed signature - accepted_gift_types: Optional[AcceptedGiftTypes] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -507,23 +493,6 @@ def __init__( is_forum=is_forum, api_kwargs=api_kwargs, ) - if accepted_gift_types is None: - raise TypeError("`accepted_gift_type` is a required argument since Bot API 9.0") - - if can_send_gift is not None: - warn( - PTBDeprecationWarning( - "22.1", - build_deprecation_warning_message( - deprecated_name="can_send_gift", - new_name="accepted_gift_types", - object_type="parameter", - bot_api_version="9.0", - ), - ), - stacklevel=2, - ) - # Required and unique to this class- with self._unfrozen(): self.max_reaction_count: int = max_reaction_count @@ -575,28 +544,8 @@ def __init__( self.business_location: Optional[BusinessLocation] = business_location self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours self.can_send_paid_media: Optional[bool] = can_send_paid_media - self._can_send_gift: Optional[bool] = can_send_gift self.accepted_gift_types: AcceptedGiftTypes = accepted_gift_types - @property - def can_send_gift(self) -> Optional[bool]: - """ - :obj:`bool`: Optional. :obj:`True`, if gifts can be sent to the chat. - - .. deprecated:: 22.1 - As Bot API 9.0 replaces this attribute with :attr:`accepted_gift_types`, this attribute - will be removed in future versions. - - """ - warn_about_deprecated_attr_in_property( - deprecated_attr_name="can_send_gift", - new_attr_name="accepted_gift_types", - bot_api_version="9.0", - ptb_version="22.1", - stacklevel=2, - ) - return self._can_send_gift - @property def slow_mode_delay(self) -> Optional[Union[int, dtm.timedelta]]: return get_timedelta_value(self._slow_mode_delay, attribute="slow_mode_delay") diff --git a/src/telegram/_payment/stars/transactionpartner.py b/src/telegram/_payment/stars/transactionpartner.py index ffe970bc6a8..a6d3d9d4979 100644 --- a/src/telegram/_payment/stars/transactionpartner.py +++ b/src/telegram/_payment/stars/transactionpartner.py @@ -416,6 +416,7 @@ class TransactionPartnerUser(TransactionPartner): def __init__( self, + transaction_type: str, user: "User", invoice_payload: Optional[str] = None, paid_media: Optional[Sequence[PaidMedia]] = None, @@ -424,17 +425,11 @@ def __init__( gift: Optional[Gift] = None, affiliate: Optional[AffiliateInfo] = None, premium_subscription_duration: Optional[int] = None, - # temporarily optional to account for changed signature - transaction_type: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) - # tags: deprecated 22.1, bot api 9.0 - if transaction_type is None: - raise TypeError("`transaction_type` is a required argument since Bot API 9.0") - with self._unfrozen(): self.user: User = user self.affiliate: Optional[AffiliateInfo] = affiliate diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 3e5777803b7..c481de5e0ae 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -27,6 +27,10 @@ .. versionchanged:: 20.0 * Most of the constants in this module are grouped into enums. + +.. versionremoved:: NEXT.VERSION + Removed deprecated class ``StarTransactions``. Please instead use + :attr:`telegram.constants.Nanostar.VALUE`. """ # TODO: Remove this when https://github.com/PyCQA/pylint/issues/6887 is resolved. # pylint: disable=invalid-enum-extension,invalid-slots @@ -103,7 +107,6 @@ "ReactionType", "ReplyLimit", "RevenueWithdrawalStateType", - "StarTransactions", "StarTransactionsLimit", "StickerFormat", "StickerLimit", @@ -2737,36 +2740,18 @@ class RevenueWithdrawalStateType(StringEnum): """:obj:`str`: A withdrawal failed and the transaction was refunded.""" -# tags: deprecated 22.1, bot api 9.0 -class StarTransactions(FloatEnum): - """This enum contains constants for :class:`telegram.StarTransaction`. - The enum members of this enumeration are instances of :class:`float` and can be treated as - such. - - .. versionadded:: 21.9 - - .. deprecated:: 22.1 - This class will be removed as its only member :attr:`NANOSTAR_VALUE` will be replaced - by :attr:`telegram.constants.Nanostar.VALUE`. - """ - - __slots__ = () - - NANOSTAR_VALUE = Nanostar.VALUE - """:obj:`float`: The value of one nanostar as used in - :attr:`telegram.StarTransaction.nanostar_amount`. - - .. deprecated:: 22.1 - This member will be replaced by :attr:`telegram.constants.Nanostar.VALUE`. - """ - - class StarTransactionsLimit(IntEnum): """This enum contains limitations for :class:`telegram.Bot.get_star_transactions` and :class:`telegram.StarTransaction`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. .. versionadded:: 21.4 + + .. versionremoved:: NEXT.VERSION + Removed deprecated attributes ``StarTransactionsLimit.NANOSTAR_MIN_AMOUNT`` + and ``StarTransactionsLimit.NANOSTAR_MAX_AMOUNT``. Please instead use + :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT` + and :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`. """ __slots__ = () @@ -2779,28 +2764,6 @@ class StarTransactionsLimit(IntEnum): """:obj:`int`: Maximum value allowed for the :paramref:`~telegram.Bot.get_star_transactions.limit` parameter of :meth:`telegram.Bot.get_star_transactions`.""" - # tags: deprecated 22.1, bot api 9.0 - NANOSTAR_MIN_AMOUNT = NanostarLimit.MIN_AMOUNT - """:obj:`int`: Minimum value allowed for :paramref:`~telegram.AffiliateInfo.nanostar_amount` - parameter of :class:`telegram.AffiliateInfo`. - - .. versionadded:: 21.9 - - .. deprecated:: 22.1 - This member will be replaced by :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT`. - """ - # tags: deprecated 22.1, bot api 9.0 - NANOSTAR_MAX_AMOUNT = NanostarLimit.MAX_AMOUNT - """:obj:`int`: Maximum value allowed for :paramref:`~telegram.StarTransaction.nanostar_amount` - parameter of :class:`telegram.StarTransaction` and - :paramref:`~telegram.AffiliateInfo.nanostar_amount` parameter of - :class:`telegram.AffiliateInfo`. - - .. versionadded:: 21.9 - - .. deprecated:: 22.1 - This member will be replaced by :attr:`telegram.constants.NanostarLimit.MAX_AMOUNT`. - """ class StickerFormat(StringEnum): diff --git a/tests/_payment/stars/test_transactionpartner.py b/tests/_payment/stars/test_transactionpartner.py index f89568901a6..53e39249090 100644 --- a/tests/_payment/stars/test_transactionpartner.py +++ b/tests/_payment/stars/test_transactionpartner.py @@ -322,19 +322,15 @@ def test_to_dict(self, transaction_partner_user): assert json_dict["subscription_period"] == self.subscription_period.total_seconds() assert json_dict["premium_subscription_duration"] == self.premium_subscription_duration - def test_transaction_type_is_required_argument(self): - with pytest.raises(TypeError, match="`transaction_type` is a required argument"): - TransactionPartnerUser(user=self.user) - def test_equality(self, transaction_partner_user): a = transaction_partner_user b = TransactionPartnerUser( - user=self.user, transaction_type=self.transaction_type, + user=self.user, ) c = TransactionPartnerUser( - user=User(id=1, is_bot=False, first_name="user", last_name="user"), transaction_type=self.transaction_type, + user=User(id=1, is_bot=False, first_name="user", last_name="user"), ) d = User(id=1, is_bot=False, first_name="user", last_name="user") diff --git a/tests/auxil/dummy_objects.py b/tests/auxil/dummy_objects.py index 9abce52fa23..f3a61583df5 100644 --- a/tests/auxil/dummy_objects.py +++ b/tests/auxil/dummy_objects.py @@ -8,6 +8,7 @@ BotDescription, BotName, BotShortDescription, + BusinessBotRights, BusinessConnection, Chat, ChatAdministratorRights, @@ -69,8 +70,8 @@ id="123", user_chat_id=123456, date=_DUMMY_DATE, - can_reply=True, is_enabled=True, + rights=BusinessBotRights(can_reply=True), ), "Chat": Chat(id=123456, type="dummy_type"), "ChatAdministratorRights": ChatAdministratorRights.all_rights(), diff --git a/tests/ext/test_businessconnectionhandler.py b/tests/ext/test_businessconnectionhandler.py index e8e8f77bdf9..25019673de3 100644 --- a/tests/ext/test_businessconnectionhandler.py +++ b/tests/ext/test_businessconnectionhandler.py @@ -23,6 +23,7 @@ from telegram import ( Bot, + BusinessBotRights, BusinessConnection, CallbackQuery, Chat, @@ -81,8 +82,8 @@ def business_connection(bot): user_chat_id=1, user=User(1, "name", username="user_a", is_bot=False), date=dtm.datetime.now(tz=UTC), - can_reply=True, is_enabled=True, + rights=BusinessBotRights(can_reply=True), ) bc.set_bot(bot) return bc diff --git a/tests/test_business_classes.py b/tests/test_business_classes.py index aabf60064c6..b1ba4ced08a 100644 --- a/tests/test_business_classes.py +++ b/tests/test_business_classes.py @@ -34,7 +34,6 @@ ) from telegram._business import BusinessBotRights from telegram._utils.datetime import UTC, to_timestamp -from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @@ -101,7 +100,6 @@ def business_connection(business_bot_rights): BusinessTestBase.user, BusinessTestBase.user_chat_id, BusinessTestBase.date, - BusinessTestBase.can_reply, BusinessTestBase.is_enabled, rights=business_bot_rights, ) @@ -246,7 +244,6 @@ def test_de_json(self, business_bot_rights): "user": self.user.to_dict(), "user_chat_id": self.user_chat_id, "date": to_timestamp(self.date), - "can_reply": self.can_reply, "is_enabled": self.is_enabled, "rights": business_bot_rights.to_dict(), } @@ -255,7 +252,6 @@ def test_de_json(self, business_bot_rights): assert bc.user == self.user assert bc.user_chat_id == self.user_chat_id assert bc.date == self.date - assert bc.can_reply == self.can_reply assert bc.is_enabled == self.is_enabled assert bc.rights == business_bot_rights assert bc.api_kwargs == {} @@ -267,7 +263,6 @@ def test_de_json_localization(self, offline_bot, raw_bot, tz_bot, business_bot_r "user": self.user.to_dict(), "user_chat_id": self.user_chat_id, "date": to_timestamp(self.date), - "can_reply": self.can_reply, "is_enabled": self.is_enabled, "rights": business_bot_rights.to_dict(), } @@ -299,7 +294,6 @@ def test_equality(self, business_bot_rights): self.user, self.user_chat_id, self.date, - self.can_reply, self.is_enabled, rights=business_bot_rights, ) @@ -308,7 +302,6 @@ def test_equality(self, business_bot_rights): self.user, self.user_chat_id, self.date, - self.can_reply, self.is_enabled, rights=business_bot_rights, ) @@ -317,7 +310,6 @@ def test_equality(self, business_bot_rights): self.user, self.user_chat_id, self.date, - self.can_reply, self.is_enabled, rights=business_bot_rights, ) @@ -326,7 +318,6 @@ def test_equality(self, business_bot_rights): self.user, self.user_chat_id, self.date, - self.can_reply, self.is_enabled, rights=BusinessBotRights(), ) @@ -340,32 +331,6 @@ def test_equality(self, business_bot_rights): assert bc1 != bc4 assert hash(bc1) != hash(bc4) - def test_can_reply_argument_property_deprecation(self, business_connection): - with pytest.warns(PTBDeprecationWarning, match=r"9\.0.*can\_reply") as record: - assert BusinessConnection( - id=self.id_, - user=self.user, - user_chat_id=self.user_chat_id, - date=self.date, - can_reply=True, - is_enabled=self.is_enabled, - ) - - assert record[0].category == PTBDeprecationWarning - assert record[0].filename == __file__, "wrong stacklevel!" - - with pytest.warns(PTBDeprecationWarning, match=r"9\.0.*can\_reply") as record: - assert business_connection.can_reply is self.can_reply - - assert record[0].category == PTBDeprecationWarning - assert record[0].filename == __file__, "wrong stacklevel!" - - def test_is_enabled_remains_required(self): - with pytest.raises(TypeError): - BusinessConnection( - id=self.id_, user=self.user, user_chat_id=self.user_chat_id, date=self.date - ) - class TestBusinessMessagesDeleted(BusinessTestBase): def test_slots(self, business_messages_deleted): diff --git a/tests/test_business_methods.py b/tests/test_business_methods.py index 721df6353b9..bdbc2e2d9b0 100644 --- a/tests/test_business_methods.py +++ b/tests/test_business_methods.py @@ -21,6 +21,7 @@ import pytest from telegram import ( + BusinessBotRights, BusinessConnection, Chat, InputProfilePhotoStatic, @@ -51,10 +52,15 @@ async def test_get_business_connection(self, offline_bot, monkeypatch): user = User(1, "first", False) user_chat_id = 1 date = dtm.datetime.utcnow() - can_reply = True + rights = BusinessBotRights(can_reply=True) is_enabled = True bc = BusinessConnection( - self.bci, user, user_chat_id, date, can_reply, is_enabled + self.bci, + user, + user_chat_id, + date, + is_enabled, + rights=rights, ).to_json() async def do_request(*args, **kwargs): diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py index 52444fcbd34..ebcdd6a71cc 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -145,7 +145,6 @@ class ChatFullInfoTestBase: first_name = "first_name" last_name = "last_name" can_send_paid_media = True - can_send_gift = True accepted_gift_types = AcceptedGiftTypes(True, True, True, True) @@ -166,7 +165,6 @@ def test_de_json(self, offline_bot): "max_reaction_count": self.max_reaction_count, "username": self.username, "accepted_gift_types": self.accepted_gift_types.to_dict(), - "can_send_gift": self.can_send_gift, "sticker_set_name": self.sticker_set_name, "can_set_sticker_set": self.can_set_sticker_set, "permissions": self.permissions.to_dict(), @@ -334,35 +332,6 @@ def test_to_dict(self, chat_full_info): assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count - def test_accepted_gift_types_is_required_argument(self): - with pytest.raises(TypeError, match="`accepted_gift_type` is a required argument"): - ChatFullInfo( - id=123, - type=Chat.PRIVATE, - accent_color_id=1, - max_reaction_count=2, - can_send_gift=True, - ) - - def test_can_send_gift_deprecation_warning(self): - with pytest.warns( - PTBDeprecationWarning, - match="'can_send_gift' was replaced by 'accepted_gift_types' in Bot API 9.0", - ): - chat_full_info = ChatFullInfo( - id=123, - type=Chat.PRIVATE, - accent_color_id=1, - max_reaction_count=2, - accepted_gift_types=self.accepted_gift_types, - can_send_gift=self.can_send_gift, - ) - with pytest.warns( - PTBDeprecationWarning, - match="Bot API 9.0 renamed the attribute 'can_send_gift' to 'accepted_gift_types'", - ): - chat_full_info.can_send_gift - def test_time_period_properties(self, PTB_TIMEDELTA, chat_full_info): cfi = chat_full_info if PTB_TIMEDELTA: diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index a1e7a8157b0..a6942837407 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -158,10 +158,6 @@ class ParamTypeCheckingExceptions: "InputPaidMedia": {"type", "media"}, # attributes common to all subclasses "InputStoryContent": {"type"}, # attributes common to all subclasses "StoryAreaType": {"type"}, # attributes common to all subclasses - # backwards compatibility for api 9.0 changes - # tags: deprecated v22.2, bot api 9.0 - "BusinessConnection": {"can_reply"}, - "ChatFullInfo": {"can_send_gift"}, "InputProfilePhoto": {"type"}, # attributes common to all subclasses } @@ -210,11 +206,6 @@ def ptb_ignored_params(object_name: str) -> set[str]: "send_venue": {"latitude", "longitude", "title", "address"}, "send_contact": {"phone_number", "first_name"}, # ----> - # backwards compatibility for api 9.0 changes - # tags: deprecated v22.2, bot api 9.0 - "BusinessConnection": {"is_enabled"}, - "ChatFullInfo": {"accepted_gift_types"}, - "TransactionPartnerUser": {"transaction_type"}, } diff --git a/tests/test_update.py b/tests/test_update.py index 46fdb88c450..480eb3758b9 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -23,6 +23,7 @@ import pytest from telegram import ( + BusinessBotRights, BusinessConnection, BusinessMessagesDeleted, CallbackQuery, @@ -129,7 +130,7 @@ 1, from_timestamp(int(time.time())), True, - True, + rights=BusinessBotRights(can_reply=True), ) deleted_business_messages = BusinessMessagesDeleted( From 957345f6d959494bfb93b5223f3f8ea7e07b7ed3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:22:20 +0200 Subject: [PATCH 07/11] Bump `pre-commit` Hooks to Latest Versions (#4858) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- .pre-commit-config.yaml | 10 +++++----- changes/unreleased/4858.ajt46xDsbfzFqcghJ2rP6g.toml | 5 +++++ src/telegram/__main__.py | 2 +- src/telegram/_chatfullinfo.py | 2 +- src/telegram/_giveaway.py | 4 +++- src/telegram/_message.py | 6 +++--- src/telegram/_passport/encryptedpassportelement.py | 1 - src/telegram/_utils/files.py | 2 +- src/telegram/ext/_application.py | 10 ++++++---- src/telegram/ext/_handlers/conversationhandler.py | 2 +- src/telegram/ext/filters.py | 4 ++-- src/telegram/helpers.py | 5 ++++- tests/_utils/test_datetime.py | 2 +- tests/ext/_utils/test_stack.py | 4 ++-- tests/ext/test_conversationhandler.py | 3 +-- tests/ext/test_defaults.py | 2 +- tests/test_official/helpers.py | 2 +- 17 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 changes/unreleased/4858.ajt46xDsbfzFqcghJ2rP6g.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a6002c846bf..4b091946e44 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.9' + rev: 'v0.12.2' hooks: - id: ruff name: ruff @@ -25,11 +25,11 @@ repos: - --diff - --check - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint - rev: v3.3.6 + rev: v3.3.7 hooks: - id: pylint files: ^(?!(tests|docs)).*\.py$ @@ -41,7 +41,7 @@ repos: - aiolimiter~=1.1,<1.3 - . # this basically does `pip install -e .` - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.1 hooks: - id: mypy name: mypy-ptb @@ -68,7 +68,7 @@ repos: - cachetools>=5.3.3,<5.5.0 - . # this basically does `pip install -e .` - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade args: diff --git a/changes/unreleased/4858.ajt46xDsbfzFqcghJ2rP6g.toml b/changes/unreleased/4858.ajt46xDsbfzFqcghJ2rP6g.toml new file mode 100644 index 00000000000..54620eebed2 --- /dev/null +++ b/changes/unreleased/4858.ajt46xDsbfzFqcghJ2rP6g.toml @@ -0,0 +1,5 @@ +internal = "Bump `pre-commit` Hooks to Latest Versions" +[[pull_requests]] +uid = "4858" +author_uid = "pre-commit-ci" +closes_threads = [] diff --git a/src/telegram/__main__.py b/src/telegram/__main__.py index 7d291b2ae1e..90e349eca5d 100644 --- a/src/telegram/__main__.py +++ b/src/telegram/__main__.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. # pylint: disable=missing-module-docstring -# ruff: noqa: T201, D100, S603, S607 +# ruff: noqa: T201, D100, S607 import subprocess import sys from typing import Optional diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index e1abdb65a1e..01439536b84 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -573,7 +573,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo": data.get("accepted_gift_types"), AcceptedGiftTypes, bot ) - from telegram import ( # pylint: disable=import-outside-toplevel + from telegram import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 BusinessIntro, BusinessLocation, BusinessOpeningHours, diff --git a/src/telegram/_giveaway.py b/src/telegram/_giveaway.py index d7d086e6548..e41f8472b8f 100644 --- a/src/telegram/_giveaway.py +++ b/src/telegram/_giveaway.py @@ -371,7 +371,9 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GiveawayComple data = cls._parse_data(data) # Unfortunately, this needs to be here due to cyclic imports - from telegram._message import Message # pylint: disable=import-outside-toplevel + from telegram._message import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + Message, + ) data["giveaway_message"] = de_json_optional(data.get("giveaway_message"), Message, bot) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 274089bff50..7736fce459f 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -1402,16 +1402,16 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": ) # Unfortunately, this needs to be here due to cyclic imports - from telegram._giveaway import ( # pylint: disable=import-outside-toplevel + from telegram._giveaway import ( # pylint: disable=C0415 # noqa: PLC0415 Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners, ) - from telegram._messageorigin import ( # pylint: disable=import-outside-toplevel + from telegram._messageorigin import ( # pylint: disable=C0415 # noqa: PLC0415 MessageOrigin, ) - from telegram._reply import ( # pylint: disable=import-outside-toplevel + from telegram._reply import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 ExternalReplyInfo, TextQuote, ) diff --git a/src/telegram/_passport/encryptedpassportelement.py b/src/telegram/_passport/encryptedpassportelement.py index c231c51640b..65f88e7a69b 100644 --- a/src/telegram/_passport/encryptedpassportelement.py +++ b/src/telegram/_passport/encryptedpassportelement.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# flake8: noqa: E501 # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2025 # Leandro Toledo de Souza diff --git a/src/telegram/_utils/files.py b/src/telegram/_utils/files.py index a750e1512e1..9bacce86f3c 100644 --- a/src/telegram/_utils/files.py +++ b/src/telegram/_utils/files.py @@ -134,7 +134,7 @@ def parse_file_input( # pylint: disable=too-many-return-statements :attr:`file_input`, in case it's no valid file input. """ # Importing on file-level yields cyclic Import Errors - from telegram import InputFile # pylint: disable=import-outside-toplevel + from telegram import InputFile # pylint: disable=import-outside-toplevel # noqa: PLC0415 if isinstance(file_input, str) and file_input.startswith("file://"): if not local_mode: diff --git a/src/telegram/ext/_application.py b/src/telegram/ext/_application.py index d287b3a375d..51f739b3e27 100644 --- a/src/telegram/ext/_application.py +++ b/src/telegram/ext/_application.py @@ -455,7 +455,9 @@ def builder() -> "InitApplicationBuilder": .. versionadded:: 20.0 """ # Unfortunately this needs to be here due to cyclical imports - from telegram.ext import ApplicationBuilder # pylint: disable=import-outside-toplevel + from telegram.ext import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + ApplicationBuilder, + ) return ApplicationBuilder() @@ -498,7 +500,7 @@ async def initialize(self) -> None: # Unfortunately due to circular imports this has to be here # pylint: disable=import-outside-toplevel - from telegram.ext._handlers.conversationhandler import ConversationHandler + from telegram.ext._handlers.conversationhandler import ConversationHandler # noqa: PLC0415 # Initialize the persistent conversation handlers with the stored states for handler in itertools.chain.from_iterable(self.handlers.values()): @@ -1369,7 +1371,7 @@ def add_handler(self, handler: BaseHandler[Any, CCT, Any], group: int = DEFAULT_ """ # Unfortunately due to circular imports this has to be here # pylint: disable=import-outside-toplevel - from telegram.ext._handlers.conversationhandler import ConversationHandler + from telegram.ext._handlers.conversationhandler import ConversationHandler # noqa: PLC0415 if not isinstance(handler, BaseHandler): raise TypeError(f"handler is not an instance of {BaseHandler.__name__}") @@ -1735,7 +1737,7 @@ async def __update_persistence(self) -> None: # Unfortunately due to circular imports this has to be here # pylint: disable=import-outside-toplevel - from telegram.ext._handlers.conversationhandler import PendingState + from telegram.ext._handlers.conversationhandler import PendingState # noqa: PLC0415 for name, (key, new_state) in itertools.chain.from_iterable( zip(itertools.repeat(name), states_dict.pop_accessed_write_items()) diff --git a/src/telegram/ext/_handlers/conversationhandler.py b/src/telegram/ext/_handlers/conversationhandler.py index dd824bf5601..d0a3734d61b 100644 --- a/src/telegram/ext/_handlers/conversationhandler.py +++ b/src/telegram/ext/_handlers/conversationhandler.py @@ -298,7 +298,7 @@ def __init__( block: DVType[bool] = DEFAULT_TRUE, ): # these imports need to be here because of circular import error otherwise - from telegram.ext import ( # pylint: disable=import-outside-toplevel + from telegram.ext import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 PollAnswerHandler, PollHandler, PreCheckoutQueryHandler, diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index 6322dafd296..f3272147442 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -2707,7 +2707,7 @@ def user_ids(self) -> frozenset[int]: @user_ids.setter def user_ids(self, user_id: SCT[int]) -> None: - self.chat_ids = user_id # type: ignore[assignment] + self.chat_ids = user_id def add_user_ids(self, user_id: SCT[int]) -> None: """ @@ -2845,7 +2845,7 @@ def bot_ids(self) -> frozenset[int]: @bot_ids.setter def bot_ids(self, bot_id: SCT[int]) -> None: - self.chat_ids = bot_id # type: ignore[assignment] + self.chat_ids = bot_id def add_bot_ids(self, bot_id: SCT[int]) -> None: """ diff --git a/src/telegram/helpers.py b/src/telegram/helpers.py index 81dd4b6c11a..494f26b716f 100644 --- a/src/telegram/helpers.py +++ b/src/telegram/helpers.py @@ -125,7 +125,10 @@ def effective_message_type(entity: Union["Message", "Update"]) -> Optional[str]: """ # Importing on file-level yields cyclic Import Errors - from telegram import Message, Update # pylint: disable=import-outside-toplevel + from telegram import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 + Message, + Update, + ) if isinstance(entity, Message): message = entity diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py index 8628f0c109f..06d252a64a2 100644 --- a/tests/_utils/test_datetime.py +++ b/tests/_utils/test_datetime.py @@ -65,7 +65,7 @@ def test_localize_utc(self): @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed") def test_localize_pytz(self): dt = dtm.datetime(2023, 1, 1, 12, 0, 0) - import pytz + import pytz # noqa: PLC0415 tzinfo = pytz.timezone("Europe/Berlin") localized_dt = tg_dtm.localize(dt, tzinfo) diff --git a/tests/ext/_utils/test_stack.py b/tests/ext/_utils/test_stack.py index 369098685c0..41fa1a61446 100644 --- a/tests/ext/_utils/test_stack.py +++ b/tests/ext/_utils/test_stack.py @@ -80,7 +80,7 @@ def caller_func(): symlink_to(symlink_file, temp_file) sys.path.append(tmp_path.as_posix()) - from caller_link import caller_func + from caller_link import caller_func # noqa: PLC0415 frame = caller_func() assert was_called_by(frame, temp_file) @@ -111,7 +111,7 @@ def outer_func(): symlink_to(symlink_file2, temp_file2) sys.path.append(tmp_path.as_posix()) - from outer_link import outer_func + from outer_link import outer_func # noqa: PLC0415 frame = outer_func() assert was_called_by(frame, temp_file2) diff --git a/tests/ext/test_conversationhandler.py b/tests/ext/test_conversationhandler.py index 64959f47f47..4a5ceb68394 100644 --- a/tests/ext/test_conversationhandler.py +++ b/tests/ext/test_conversationhandler.py @@ -20,6 +20,7 @@ import asyncio import functools import logging +from copy import copy from pathlib import Path from warnings import filterwarnings @@ -307,8 +308,6 @@ def test_repr_no_truncation(self): ) def test_repr_with_truncation(self): - from copy import copy - states = copy(self.drinking_states) # there are exactly 3 drinking states. adding one more to make sure it's truncated states["extra_to_be_truncated"] = [CommandHandler("foo", self.start)] diff --git a/tests/ext/test_defaults.py b/tests/ext/test_defaults.py index 32f4b0c3800..a6c6ca3b047 100644 --- a/tests/ext/test_defaults.py +++ b/tests/ext/test_defaults.py @@ -42,7 +42,7 @@ def test_utc(self): @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="pytz not installed") def test_pytz_deprecation(self, recwarn): - import pytz + import pytz # noqa: PLC0415 with pytest.warns(PTBDeprecationWarning, match="pytz") as record: Defaults(tzinfo=pytz.timezone("Europe/Berlin")) diff --git a/tests/test_official/helpers.py b/tests/test_official/helpers.py index f2fcf890344..8fb08a01bd5 100644 --- a/tests/test_official/helpers.py +++ b/tests/test_official/helpers.py @@ -93,7 +93,7 @@ def is_parameter_required_by_tg(field: str) -> bool: def wrap_with_none(tg_parameter: "TelegramParameter", mapped_type: Any, obj: object) -> type: """Adds `None` to type annotation if the parameter isn't required. Respects ignored params.""" # have to import here to avoid circular imports - from tests.test_official.exceptions import ignored_param_requirements + from tests.test_official.exceptions import ignored_param_requirements # noqa: PLC0415 if tg_parameter.param_name in ignored_param_requirements(obj.__name__): return mapped_type | type(None) From e9dd490b2ca43d3f4f288e579f64acfda01f221f Mon Sep 17 00:00:00 2001 From: Xiaohu <975326527@qq.com> Date: Tue, 15 Jul 2025 23:00:54 +0800 Subject: [PATCH 08/11] Make Gender Input Case-Insensitive in ``conversationbot.py`` (#4855) --- changes/unreleased/4855.8hCFRFMeMaRWpBEYaxrTMq.toml | 5 +++++ examples/conversationbot.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/4855.8hCFRFMeMaRWpBEYaxrTMq.toml diff --git a/changes/unreleased/4855.8hCFRFMeMaRWpBEYaxrTMq.toml b/changes/unreleased/4855.8hCFRFMeMaRWpBEYaxrTMq.toml new file mode 100644 index 00000000000..cbbda59a270 --- /dev/null +++ b/changes/unreleased/4855.8hCFRFMeMaRWpBEYaxrTMq.toml @@ -0,0 +1,5 @@ +other= "Make Gender Input Case-Insensitive in ``conversationbot.py``" +[[pull_requests]] +uid = "4855" +author_uid = "fengxiaohu" +closes_threads = ["4846"] diff --git a/examples/conversationbot.py b/examples/conversationbot.py index 751f48f74e0..dbe637c1203 100644 --- a/examples/conversationbot.py +++ b/examples/conversationbot.py @@ -145,7 +145,9 @@ def main() -> None: conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ - GENDER: [MessageHandler(filters.Regex("^(Boy|Girl|Other)$"), gender)], + # Use case-insensitive regex to accept gender input regardless of letter casing, + # e.g., "boy", "BOY", "Girl", etc., will all be matched + GENDER: [MessageHandler(filters.Regex("(?i)^(Boy|Girl|Other)$"), gender)], PHOTO: [MessageHandler(filters.PHOTO, photo), CommandHandler("skip", skip_photo)], LOCATION: [ MessageHandler(filters.LOCATION, location), From 1111d342d6b0345954a382aa815ce8b783f3f7bd Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 20 Jul 2025 21:39:21 +0200 Subject: [PATCH 09/11] Full Support for Bot API 9.1 (#4847) Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> --- README.rst | 4 +- .../4847.8ujbbBbaZ2VTEdRLeqirSZ.toml | 18 + docs/source/inclusions/bot_methods.rst | 6 + docs/source/telegram.at-tree.rst | 7 + docs/source/telegram.checklist.rst | 6 + docs/source/telegram.checklisttask.rst | 6 + docs/source/telegram.checklisttasksadded.rst | 6 + docs/source/telegram.checklisttasksdone.rst | 6 + .../telegram.directmessagepricechanged.rst | 6 + docs/source/telegram.inputchecklist.rst | 6 + docs/source/telegram.inputchecklisttask.rst | 6 + src/telegram/__init__.py | 10 + src/telegram/_bot.py | 173 +++++++ src/telegram/_callbackquery.py | 38 ++ src/telegram/_chat.py | 49 ++ src/telegram/_checklists.py | 392 ++++++++++++++++ src/telegram/_directmessagepricechanged.py | 73 +++ src/telegram/_inputchecklist.py | 186 ++++++++ src/telegram/_message.py | 163 +++++++ src/telegram/_ownedgift.py | 18 +- src/telegram/_reply.py | 11 + src/telegram/_uniquegift.py | 50 +- src/telegram/constants.py | 75 ++- src/telegram/ext/_extbot.py | 88 ++++ src/telegram/ext/filters.py | 56 +++ tests/ext/test_filters.py | 24 +- tests/test_bot.py | 17 + tests/test_business_methods.py | 151 ++++++ tests/test_callbackquery.py | 49 +- tests/test_chat.py | 27 +- tests/test_checklists.py | 439 ++++++++++++++++++ tests/test_directmessagepricechanged.py | 86 ++++ tests/test_inputchecklist.py | 167 +++++++ tests/test_message.py | 110 ++++- tests/test_ownedgift.py | 9 +- tests/test_reply.py | 13 + tests/test_uniquegift.py | 42 ++ 37 files changed, 2569 insertions(+), 24 deletions(-) create mode 100644 changes/unreleased/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml create mode 100644 docs/source/telegram.checklist.rst create mode 100644 docs/source/telegram.checklisttask.rst create mode 100644 docs/source/telegram.checklisttasksadded.rst create mode 100644 docs/source/telegram.checklisttasksdone.rst create mode 100644 docs/source/telegram.directmessagepricechanged.rst create mode 100644 docs/source/telegram.inputchecklist.rst create mode 100644 docs/source/telegram.inputchecklisttask.rst create mode 100644 src/telegram/_checklists.py create mode 100644 src/telegram/_directmessagepricechanged.py create mode 100644 src/telegram/_inputchecklist.py create mode 100644 tests/test_checklists.py create mode 100644 tests/test_directmessagepricechanged.py create mode 100644 tests/test_inputchecklist.py diff --git a/README.rst b/README.rst index a1aa26871e8..9d0ff953ba7 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-9.0-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-9.1-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **9.0** are natively supported by this library. +All types and methods of the Telegram Bot API **9.1** are natively supported by this library. In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_. Notable Features diff --git a/changes/unreleased/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml b/changes/unreleased/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml new file mode 100644 index 00000000000..82d84835c8e --- /dev/null +++ b/changes/unreleased/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml @@ -0,0 +1,18 @@ +highlights = "Full Support for Bot API 9.1" + +features = """ +New filters based on Bot API 9.1: + +* ``filters.StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED`` for ``Message.direct_message_price_changed`` +* ``filters.StatusUpdate.CHECKLIST_TASKS_ADDED`` for ``Message.checklist_tasks_added`` +* ``filters.StatusUpdate.CHECKLIST_TASKS_DONE`` for ``Message.checklist_tasks_done`` +* ``filters.CHECKLIST`` for ``Message.checklist`` +""" + +pull_requests = [ + { uid = "4847", author_uid = "Bibo-Joshi", closes_threads = ["4845"] }, + { uid = "4848", author_uid = "Bibo-Joshi" }, + { uid = "4849", author_uid = "harshil21" }, + { uid = "4851", author_uid = "harshil21" }, + { uid = "4857", author_uid = "aelkheir" }, +] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index d1ff3c3ac13..470fcb17ff6 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -390,6 +390,8 @@ - Used to generate an HTTP link for an invoice * - :meth:`~telegram.Bot.edit_user_star_subscription` - Used for editing a user's star subscription + * - :meth:`~telegram.Bot.get_my_star_balance` + - Used for obtaining the bot's Telegram Stars balance * - :meth:`~telegram.Bot.get_star_transactions` - Used for obtaining the bot's Telegram Stars transactions * - :meth:`~telegram.Bot.refund_star_payment` @@ -447,6 +449,10 @@ - Used for transferring owned unique gifts to another user. * - :meth:`~telegram.Bot.transfer_business_account_stars` - Used for transfering Stars from the business account balance to the bot's balance. + * - :meth:`~telegram.Bot.send_checklist` + - Used for sending a checklist on behalf of the business account. + * - :meth:`~telegram.Bot.edit_message_checklist` + - Used for editing a checklist on behalf of the business account. .. raw:: html diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 63da86e76de..acfaf866f46 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -31,6 +31,10 @@ Available Types telegram.chat telegram.chatadministratorrights telegram.chatbackground + telegram.checklist + telegram.checklisttask + telegram.checklisttasksadded + telegram.checklisttasksdone telegram.copytextbutton telegram.backgroundtype telegram.backgroundtypefill @@ -66,6 +70,7 @@ Available Types telegram.chatshared telegram.contact telegram.dice + telegram.directmessagepricechanged telegram.document telegram.externalreplyinfo telegram.file @@ -85,6 +90,8 @@ Available Types telegram.inaccessiblemessage telegram.inlinekeyboardbutton telegram.inlinekeyboardmarkup + telegram.inputchecklist + telegram.inputchecklisttask telegram.inputfile telegram.inputmedia telegram.inputmediaanimation diff --git a/docs/source/telegram.checklist.rst b/docs/source/telegram.checklist.rst new file mode 100644 index 00000000000..a01dac43aad --- /dev/null +++ b/docs/source/telegram.checklist.rst @@ -0,0 +1,6 @@ +Checklist +========= + +.. autoclass:: telegram.Checklist + :members: + :show-inheritance: diff --git a/docs/source/telegram.checklisttask.rst b/docs/source/telegram.checklisttask.rst new file mode 100644 index 00000000000..27f44d629de --- /dev/null +++ b/docs/source/telegram.checklisttask.rst @@ -0,0 +1,6 @@ +ChecklistTask +============= + +.. autoclass:: telegram.ChecklistTask + :members: + :show-inheritance: diff --git a/docs/source/telegram.checklisttasksadded.rst b/docs/source/telegram.checklisttasksadded.rst new file mode 100644 index 00000000000..d3c33c02300 --- /dev/null +++ b/docs/source/telegram.checklisttasksadded.rst @@ -0,0 +1,6 @@ +ChecklistTasksAdded +=================== + +.. autoclass:: telegram.ChecklistTasksAdded + :members: + :show-inheritance: diff --git a/docs/source/telegram.checklisttasksdone.rst b/docs/source/telegram.checklisttasksdone.rst new file mode 100644 index 00000000000..aa1e0b83f84 --- /dev/null +++ b/docs/source/telegram.checklisttasksdone.rst @@ -0,0 +1,6 @@ +ChecklistTasksDone +================== + +.. autoclass:: telegram.ChecklistTasksDone + :members: + :show-inheritance: diff --git a/docs/source/telegram.directmessagepricechanged.rst b/docs/source/telegram.directmessagepricechanged.rst new file mode 100644 index 00000000000..64356e1a689 --- /dev/null +++ b/docs/source/telegram.directmessagepricechanged.rst @@ -0,0 +1,6 @@ +DirectMessagePriceChanged +========================= + +.. autoclass:: telegram.DirectMessagePriceChanged + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inputchecklist.rst b/docs/source/telegram.inputchecklist.rst new file mode 100644 index 00000000000..f83345884a0 --- /dev/null +++ b/docs/source/telegram.inputchecklist.rst @@ -0,0 +1,6 @@ +InputChecklist +============== + +.. autoclass:: telegram.InputChecklist + :members: + :show-inheritance: diff --git a/docs/source/telegram.inputchecklisttask.rst b/docs/source/telegram.inputchecklisttask.rst new file mode 100644 index 00000000000..1cc14095b0c --- /dev/null +++ b/docs/source/telegram.inputchecklisttask.rst @@ -0,0 +1,6 @@ +InputChecklistTask +================== + +.. autoclass:: telegram.InputChecklistTask + :members: + :show-inheritance: diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index 0f20f0ba605..b0277a7e77a 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -82,12 +82,17 @@ "ChatPermissions", "ChatPhoto", "ChatShared", + "Checklist", + "ChecklistTask", + "ChecklistTasksAdded", + "ChecklistTasksDone", "ChosenInlineResult", "Contact", "CopyTextButton", "Credentials", "DataCredentials", "Dice", + "DirectMessagePriceChanged", "Document", "EncryptedCredentials", "EncryptedPassportElement", @@ -138,6 +143,8 @@ "InlineQueryResultVideo", "InlineQueryResultVoice", "InlineQueryResultsButton", + "InputChecklist", + "InputChecklistTask", "InputContactMessageContent", "InputFile", "InputInvoiceMessageContent", @@ -302,6 +309,7 @@ "warnings", ) +from telegram._inputchecklist import InputChecklist, InputChecklistTask from telegram._payment.stars.staramount import StarAmount from telegram._payment.stars.startransactions import StarTransaction, StarTransactions from telegram._payment.stars.transactionpartner import ( @@ -381,9 +389,11 @@ ) from ._chatmemberupdated import ChatMemberUpdated from ._chatpermissions import ChatPermissions +from ._checklists import Checklist, ChecklistTask, ChecklistTasksAdded, ChecklistTasksDone from ._choseninlineresult import ChosenInlineResult from ._copytextbutton import CopyTextButton from ._dice import Dice +from ._directmessagepricechanged import DirectMessagePriceChanged from ._files._inputstorycontent import ( InputStoryContent, InputStoryContentPhoto, diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 56072fbe0d6..1577cc38d16 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -78,6 +78,7 @@ from telegram._gifts import AcceptedGiftTypes, Gift, Gifts from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._inline.preparedinlinemessage import PreparedInlineMessage +from telegram._inputchecklist import InputChecklist from telegram._menubutton import MenuButton from telegram._message import Message from telegram._messageid import MessageId @@ -7555,6 +7556,142 @@ async def stop_poll( ) return Poll.de_json(result, self) + async def send_checklist( + self, + business_connection_id: str, + chat_id: int, + checklist: InputChecklist, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_effect_id: Optional[str] = None, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> Message: + """ + Use this method to send a checklist on behalf of a connected business account. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): + |business_id_str| + chat_id (:obj:`int`): + Unique identifier for the target chat. + checklist (:class:`telegram.InputChecklist`): + The checklist to send. + disable_notification (:obj:`bool`, optional): + |disable_notification| + protect_content (:obj:`bool`, optional): + |protect_content| + message_effect_id (:obj:`str`, optional): + |message_effect_id| + reply_parameters (:class:`telegram.ReplyParameters`, optional): + |reply_parameters| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): + An object for an inline keyboard + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "checklist": checklist, + } + + return await self._send_message( + "sendChecklist", + data, + disable_notification=disable_notification, + reply_markup=reply_markup, + protect_content=protect_content, + reply_parameters=reply_parameters, + message_effect_id=message_effect_id, + business_connection_id=business_connection_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_message_checklist( + self, + business_connection_id: str, + chat_id: int, + message_id: int, + checklist: InputChecklist, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> Message: + """ + Use this method to edit a checklist on behalf of a connected business account. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): + |business_id_str| + chat_id (:obj:`int`): + Unique identifier for the target chat. + message_id (:obj:`int`): + Unique identifier for the target message. + checklist (:class:`telegram.InputChecklist`): + The new checklist. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): + An object for the new inline keyboard for the message. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "checklist": checklist, + } + + return await self._send_message( + "editMessageChecklist", + data, + reply_markup=reply_markup, + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def send_dice( self, chat_id: Union[int, str], @@ -11072,6 +11209,36 @@ async def remove_user_verification( api_kwargs=api_kwargs, ) + async def get_my_star_balance( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> StarAmount: + """A method to get the current Telegram Stars balance of the bot. Requires no parameters. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.StarAmount` + + Raises: + :class:`telegram.error.TelegramError` + """ + return StarAmount.de_json( + await self._post( + "getMyStarBalance", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -11244,6 +11411,10 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`send_poll`""" stopPoll = stop_poll """Alias for :meth:`stop_poll`""" + sendChecklist = send_checklist + """Alias for :meth:`send_checklist`""" + editMessageChecklist = edit_message_checklist + """Alias for :meth:`edit_message_checklist`""" sendDice = send_dice """Alias for :meth:`send_dice`""" getMyCommands = get_my_commands @@ -11386,3 +11557,5 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`remove_chat_verification`""" removeUserVerification = remove_user_verification """Alias for :meth:`remove_user_verification`""" + getMyStarBalance = get_my_star_balance + """Alias for :meth:`get_my_star_balance`""" diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 99b4ad115b5..49ae9159660 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -23,6 +23,7 @@ from telegram import constants from telegram._files.location import Location +from telegram._inputchecklist import InputChecklist from telegram._message import MaybeInaccessibleMessage, Message from telegram._telegramobject import TelegramObject from telegram._user import User @@ -345,6 +346,43 @@ async def edit_message_caption( show_caption_above_media=show_caption_above_media, ) + async def edit_message_checklist( + self, + checklist: InputChecklist, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> Union[Message, bool]: + """Shortcut for:: + + await update.callback_query.message.edit_checklist(*args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Message.edit_checklist`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.Message`: On success, the edited Message is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + return await self._get_message().edit_checklist( + checklist=checklist, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def edit_message_reply_markup( self, reply_markup: Optional["InlineKeyboardMarkup"] = None, diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 02eb6629d6d..4efb4fd80bb 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -53,6 +53,7 @@ Document, Gift, InlineKeyboardMarkup, + InputChecklist, InputMediaAudio, InputMediaDocument, InputMediaPhoto, @@ -1471,6 +1472,54 @@ async def send_document( allow_paid_broadcast=allow_paid_broadcast, ) + async def send_checklist( + self, + business_connection_id: str, + checklist: "InputChecklist", + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_checklist(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_checklist`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_checklist( + chat_id=self.id, + business_connection_id=business_connection_id, + checklist=checklist, + disable_notification=disable_notification, + protect_content=protect_content, + message_effect_id=message_effect_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def send_dice( self, disable_notification: ODVInput[bool] = DEFAULT_NONE, diff --git a/src/telegram/_checklists.py b/src/telegram/_checklists.py new file mode 100644 index 00000000000..818fd5981f5 --- /dev/null +++ b/src/telegram/_checklists.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an objects related to Telegram checklists.""" +import datetime as dtm +from collections.abc import Sequence +from typing import TYPE_CHECKING, Optional + +from telegram._messageentity import MessageEntity +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.entities import parse_message_entities, parse_message_entity +from telegram._utils.types import JSONDict +from telegram.constants import ZERO_DATE + +if TYPE_CHECKING: + from telegram import Bot, Message + + +class ChecklistTask(TelegramObject): + """ + Describes a task in a checklist. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if all their :attr:`id` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + id (:obj:`int`): Unique identifier of the task. + text (:obj:`str`): Text of the task. + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special + entities that appear in the task text. + completed_by_user (:class:`telegram.User`, optional): User that completed the task; omitted + if the task wasn't completed + completion_date (:class:`datetime.datetime`, optional): Point in time when + the task was completed; :attr:`~telegram.constants.ZERO_DATE` if the task wasn't + completed + + |datetime_localization| + + Attributes: + id (:obj:`int`): Unique identifier of the task. + text (:obj:`str`): Text of the task. + text_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special + entities that appear in the task text. + completed_by_user (:class:`telegram.User`): Optional. User that completed the task; omitted + if the task wasn't completed + completion_date (:class:`datetime.datetime`): Optional. Point in time when + the task was completed; :attr:`~telegram.constants.ZERO_DATE` if the task wasn't + completed + + |datetime_localization| + """ + + __slots__ = ( + "completed_by_user", + "completion_date", + "id", + "text", + "text_entities", + ) + + def __init__( + self, + id: int, # pylint: disable=redefined-builtin + text: str, + text_entities: Optional[Sequence[MessageEntity]] = None, + completed_by_user: Optional[User] = None, + completion_date: Optional[dtm.datetime] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.id: int = id + self.text: str = text + self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) + self.completed_by_user: Optional[User] = completed_by_user + self.completion_date: Optional[dtm.datetime] = completion_date + + self._id_attrs = (self.id,) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTask": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + if (date := data.get("completion_date")) == 0: + data["completion_date"] = ZERO_DATE + else: + data["completion_date"] = from_timestamp(date, tzinfo=loc_tzinfo) + + data["completed_by_user"] = de_json_optional(data.get("completed_by_user"), User, bot) + data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, 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 ``ChecklistTask.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:`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 checklist task 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. + + 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) + + +class Checklist(TelegramObject): + """ + Describes a checklist. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if all their :attr:`tasks` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + title (:obj:`str`): Title of the checklist. + title_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special + entities that appear in the checklist title. + tasks (Sequence[:class:`telegram.ChecklistTask`]): List of tasks in the checklist. + others_can_add_tasks (:obj:`bool`, optional): :obj:`True` if users other than the creator + of the list can add tasks to the list + others_can_mark_tasks_as_done (:obj:`bool`, optional): :obj:`True` if users other than the + creator of the list can mark tasks as done or not done + + Attributes: + title (:obj:`str`): Title of the checklist. + title_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special + entities that appear in the checklist title. + tasks (Tuple[:class:`telegram.ChecklistTask`]): List of tasks in the checklist. + others_can_add_tasks (:obj:`bool`): Optional. :obj:`True` if users other than the creator + of the list can add tasks to the list + others_can_mark_tasks_as_done (:obj:`bool`): Optional. :obj:`True` if users other than the + creator of the list can mark tasks as done or not done + """ + + __slots__ = ( + "others_can_add_tasks", + "others_can_mark_tasks_as_done", + "tasks", + "title", + "title_entities", + ) + + def __init__( + self, + title: str, + tasks: Sequence[ChecklistTask], + title_entities: Optional[Sequence[MessageEntity]] = None, + others_can_add_tasks: Optional[bool] = None, + others_can_mark_tasks_as_done: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.title: str = title + self.title_entities: tuple[MessageEntity, ...] = parse_sequence_arg(title_entities) + self.tasks: tuple[ChecklistTask, ...] = parse_sequence_arg(tasks) + self.others_can_add_tasks: Optional[bool] = others_can_add_tasks + self.others_can_mark_tasks_as_done: Optional[bool] = others_can_mark_tasks_as_done + + self._id_attrs = (self.tasks,) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Checklist": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + data["title_entities"] = de_list_optional(data.get("title_entities"), MessageEntity, bot) + data["tasks"] = de_list_optional(data.get("tasks"), ChecklistTask, bot) + + return super().de_json(data=data, bot=bot) + + def parse_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`title` + from a given :class:`telegram.MessageEntity` of :attr:`title_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 :attr:`title` 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:`title_entities`. + + Returns: + :obj:`str`: The text of the given entity. + """ + return parse_message_entity(self.title, 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 checklist's title 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:`title_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_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.title, self.title_entities, types) + + +class ChecklistTasksDone(TelegramObject): + """ + Describes a service message about checklist tasks marked as done or not done. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their :attr:`marked_as_done_task_ids` and + :attr:`marked_as_not_done_task_ids` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + checklist_message (:class:`telegram.Message`, optional): Message containing the checklist + whose tasks were marked as done or not done. Note that the ~:class:`telegram.Message` + object in this field will not contain the :attr:`~telegram.Message.reply_to_message` + field even if it itself is a reply. + marked_as_done_task_ids (Sequence[:obj:`int`], optional): Identifiers of the tasks that + were marked as done + marked_as_not_done_task_ids (Sequence[:obj:`int`], optional): Identifiers of the tasks that + were marked as not done + + Attributes: + checklist_message (:class:`telegram.Message`): Optional. Message containing the checklist + whose tasks were marked as done or not done. Note that the ~:class:`telegram.Message` + object in this field will not contain the :attr:`~telegram.Message.reply_to_message` + field even if it itself is a reply. + marked_as_done_task_ids (Tuple[:obj:`int`]): Optional. Identifiers of the tasks that were + marked as done + marked_as_not_done_task_ids (Tuple[:obj:`int`]): Optional. Identifiers of the tasks that + were marked as not done + """ + + __slots__ = ( + "checklist_message", + "marked_as_done_task_ids", + "marked_as_not_done_task_ids", + ) + + def __init__( + self, + checklist_message: Optional["Message"] = None, + marked_as_done_task_ids: Optional[Sequence[int]] = None, + marked_as_not_done_task_ids: Optional[Sequence[int]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.checklist_message: Optional[Message] = checklist_message + self.marked_as_done_task_ids: tuple[int, ...] = parse_sequence_arg(marked_as_done_task_ids) + self.marked_as_not_done_task_ids: tuple[int, ...] = parse_sequence_arg( + marked_as_not_done_task_ids + ) + + self._id_attrs = (self.marked_as_done_task_ids, self.marked_as_not_done_task_ids) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTasksDone": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + # needs to be imported here to avoid circular import issues + from telegram import Message # pylint: disable=import-outside-toplevel # noqa: PLC0415 + + data["checklist_message"] = de_json_optional(data.get("checklist_message"), Message, bot) + + return super().de_json(data=data, bot=bot) + + +class ChecklistTasksAdded(TelegramObject): + """ + Describes a service message about tasks added to a checklist. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their :attr:`tasks` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + checklist_message (:class:`telegram.Message`, optional): Message containing the checklist + to which tasks were added. Note that the ~:class:`telegram.Message` + object in this field will not contain the :attr:`~telegram.Message.reply_to_message` + field even if it itself is a reply. + tasks (Sequence[:class:`telegram.ChecklistTask`]): List of tasks added to the checklist + + Attributes: + checklist_message (:class:`telegram.Message`): Optional. Message containing the checklist + to which tasks were added. Note that the ~:class:`telegram.Message` + object in this field will not contain the :attr:`~telegram.Message.reply_to_message` + field even if it itself is a reply. + tasks (Tuple[:class:`telegram.ChecklistTask`]): List of tasks added to the checklist + """ + + __slots__ = ("checklist_message", "tasks") + + def __init__( + self, + tasks: Sequence[ChecklistTask], + checklist_message: Optional["Message"] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.checklist_message: Optional[Message] = checklist_message + self.tasks: tuple[ChecklistTask, ...] = parse_sequence_arg(tasks) + + self._id_attrs = (self.tasks,) + + self._freeze() + + @classmethod + def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTasksAdded": + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + # needs to be imported here to avoid circular import issues + from telegram import Message # pylint: disable=import-outside-toplevel # noqa: PLC0415 + + data["checklist_message"] = de_json_optional(data.get("checklist_message"), Message, bot) + data["tasks"] = ChecklistTask.de_list(data.get("tasks", []), bot) + + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_directmessagepricechanged.py b/src/telegram/_directmessagepricechanged.py new file mode 100644 index 00000000000..9d4ed925137 --- /dev/null +++ b/src/telegram/_directmessagepricechanged.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Direct Message Price.""" + + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class DirectMessagePriceChanged(TelegramObject): + """ + Describes a service message about a change in the price of direct messages sent to a channel + chat. + + .. versionadded:: NEXT.VERSION + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`are_direct_messages_enabled`, and + :attr:`direct_message_star_count` are equal. + + Args: + are_direct_messages_enabled (:obj:`bool`): + :obj:`True`, if direct messages are enabled for the channel chat; :obj:`False` + otherwise. + direct_message_star_count (:obj:`int`, optional): + The new number of Telegram Stars that must be paid by users for each direct message + sent to the channel. Does not apply to users who have been exempted by administrators. + Defaults to ``0``. + + Attributes: + are_direct_messages_enabled (:obj:`bool`): + :obj:`True`, if direct messages are enabled for the channel chat; :obj:`False` + otherwise. + direct_message_star_count (:obj:`int`): + Optional. The new number of Telegram Stars that must be paid by users for each direct + message sent to the channel. Does not apply to users who have been exempted by + administrators. Defaults to ``0``. + """ + + __slots__ = ("are_direct_messages_enabled", "direct_message_star_count") + + def __init__( + self, + are_direct_messages_enabled: bool, + direct_message_star_count: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.are_direct_messages_enabled: bool = are_direct_messages_enabled + self.direct_message_star_count: Optional[int] = direct_message_star_count + + self._id_attrs = (self.are_direct_messages_enabled, self.direct_message_star_count) + + self._freeze() diff --git a/src/telegram/_inputchecklist.py b/src/telegram/_inputchecklist.py new file mode 100644 index 00000000000..2608c99ccf7 --- /dev/null +++ b/src/telegram/_inputchecklist.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an objects that are related to Telegram input checklists.""" +from collections.abc import Sequence +from typing import Optional + +from telegram._messageentity import MessageEntity +from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput + + +class InputChecklistTask(TelegramObject): + """ + Describes a task to add to a checklist. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal if their :attr:`id` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + id (:obj:`int`): + Unique identifier of the task; must be positive and unique among all task identifiers + currently present in the checklist. + text (:obj:`str`): + Text of the task; + :tg-const:`telegram.constants.InputChecklistLimit.MIN_TEXT_LENGTH`\ +-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TEXT_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): + |parse_mode| + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): + List of special entities that appear in the text, which can be specified instead of + parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, and + custom_emoji entities are allowed. + + Attributes: + id (:obj:`int`): + Unique identifier of the task; must be positive and unique among all task identifiers + currently present in the checklist. + text (:obj:`str`): + Text of the task; + :tg-const:`telegram.constants.InputChecklistLimit.MIN_TEXT_LENGTH`\ +-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TEXT_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`): + Optional. |parse_mode| + text_entities (Sequence[:class:`telegram.MessageEntity`]): + Optional. List of special entities that appear in the text, which can be specified + instead of parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, + and custom_emoji entities are allowed. + + """ + + __slots__ = ( + "id", + "parse_mode", + "text", + "text_entities", + ) + + def __init__( + self, + id: int, # pylint: disable=redefined-builtin + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.id: int = id + self.text: str = text + self.parse_mode: ODVInput[str] = parse_mode + self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) + + self._id_attrs = (self.id,) + + self._freeze() + + +class InputChecklist(TelegramObject): + """ + Describes a checklist to create. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal if their :attr:`tasks` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + title (:obj:`str`): + Title of the checklist; + :tg-const:`telegram.constants.InputChecklistLimit.MIN_TITLE_LENGTH`\ +-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TITLE_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): + |parse_mode| + title_entities (Sequence[:class:`telegram.MessageEntity`], optional): + List of special entities that appear in the title, which + can be specified instead of :paramref:`parse_mode`. Currently, only bold, italic, + underline, strikethrough, spoiler, and custom_emoji entities are allowed. + tasks (Sequence[:class:`telegram.InputChecklistTask`]): + List of + :tg-const:`telegram.constants.InputChecklistLimit.MIN_TASK_NUMBER`\ +-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TASK_NUMBER` tasks in + the checklist. + others_can_add_tasks (:obj:`bool`, optional): + Pass :obj:`True` if other users can add tasks to the checklist. + others_can_mark_tasks_as_done (:obj:`bool`, optional): + Pass :obj:`True` if other users can mark tasks as done or not done in the checklist. + + Attributes: + title (:obj:`str`): + Title of the checklist; + :tg-const:`telegram.constants.InputChecklistLimit.MIN_TITLE_LENGTH`\ +-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TITLE_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`): + Optional. |parse_mode| + title_entities (Sequence[:class:`telegram.MessageEntity`]): + Optional. List of special entities that appear in the title, which + can be specified instead of :paramref:`parse_mode`. Currently, only bold, italic, + underline, strikethrough, spoiler, and custom_emoji entities are allowed. + tasks (Sequence[:class:`telegram.InputChecklistTask`]): + List of + :tg-const:`telegram.constants.InputChecklistLimit.MIN_TASK_NUMBER`\ +-:tg-const:`telegram.constants.InputChecklistLimit.MAX_TASK_NUMBER` tasks in + the checklist. + others_can_add_tasks (:obj:`bool`): + Optional. Pass :obj:`True` if other users can add tasks to the checklist. + others_can_mark_tasks_as_done (:obj:`bool`): + Optional. Pass :obj:`True` if other users can mark tasks as done or not done in + the checklist. + + """ + + __slots__ = ( + "others_can_add_tasks", + "others_can_mark_tasks_as_done", + "parse_mode", + "tasks", + "title", + "title_entities", + ) + + def __init__( + self, + title: str, + tasks: Sequence[InputChecklistTask], + parse_mode: ODVInput[str] = DEFAULT_NONE, + title_entities: Optional[Sequence[MessageEntity]] = None, + others_can_add_tasks: Optional[bool] = None, + others_can_mark_tasks_as_done: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.title: str = title + self.tasks: tuple[InputChecklistTask, ...] = parse_sequence_arg(tasks) + self.parse_mode: ODVInput[str] = parse_mode + self.title_entities: tuple[MessageEntity, ...] = parse_sequence_arg(title_entities) + self.others_can_add_tasks: Optional[bool] = others_can_add_tasks + self.others_can_mark_tasks_as_done: Optional[bool] = others_can_mark_tasks_as_done + + self._id_attrs = (self.tasks,) + + self._freeze() diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 7736fce459f..d1a155ce4d7 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -28,7 +28,9 @@ from telegram._chat import Chat from telegram._chatbackground import ChatBackground from telegram._chatboost import ChatBoostAdded +from telegram._checklists import Checklist, ChecklistTasksAdded, ChecklistTasksDone from telegram._dice import Dice +from telegram._directmessagepricechanged import DirectMessagePriceChanged from telegram._files.animation import Animation from telegram._files.audio import Audio from telegram._files.contact import Contact @@ -51,6 +53,7 @@ from telegram._games.game import Game from telegram._gifts import GiftInfo from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inputchecklist import InputChecklist from telegram._linkpreviewoptions import LinkPreviewOptions from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from telegram._messageentity import MessageEntity @@ -524,6 +527,9 @@ class Message(MaybeInaccessibleMessage): by a spoiler animation. .. versionadded:: 20.0 + checklist (:class:`telegram.Checklist`, optional): Message is a checklist + + .. versionadded:: NEXT.VERSION users_shared (:class:`telegram.UsersShared`, optional): Service message: users were shared with the bot @@ -601,6 +607,14 @@ class Message(MaybeInaccessibleMessage): background set. .. versionadded:: 21.2 + checklist_tasks_done (:class:`telegram.ChecklistTasksDone`, optional): Service message: + some tasks in a checklist were marked as done or not done + + .. versionadded:: NEXT.VERSION + checklist_tasks_added (:class:`telegram.ChecklistTasksAdded`, optional): Service message: + tasks were added to a checklist + + .. versionadded:: NEXT.VERSION paid_media (:class:`telegram.PaidMediaInfo`, optional): Message contains paid media; information about the paid media. @@ -609,6 +623,11 @@ class Message(MaybeInaccessibleMessage): message about a refunded payment, information about the payment. .. versionadded:: 21.4 + direct_message_price_changed (:class:`telegram.DirectMessagePriceChanged`, optional): + Service message: the price for paid messages in the corresponding direct messages chat + of a channel has changed. + + .. versionadded:: NEXT.VERSION Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. In specific instances @@ -868,6 +887,9 @@ class Message(MaybeInaccessibleMessage): by a spoiler animation. .. versionadded:: 20.0 + checklist (:class:`telegram.Checklist`): Optional. Message is a checklist + + .. versionadded:: NEXT.VERSION users_shared (:class:`telegram.UsersShared`): Optional. Service message: users were shared with the bot @@ -946,6 +968,14 @@ class Message(MaybeInaccessibleMessage): background set .. versionadded:: 21.2 + checklist_tasks_done (:class:`telegram.ChecklistTasksDone`): Optional. Service message: + some tasks in a checklist were marked as done or not done + + .. versionadded:: NEXT.VERSION + checklist_tasks_added (:class:`telegram.ChecklistTasksAdded`): Optional. Service message: + tasks were added to a checklist + + .. versionadded:: NEXT.VERSION paid_media (:class:`telegram.PaidMediaInfo`): Optional. Message contains paid media; information about the paid media. @@ -954,6 +984,11 @@ class Message(MaybeInaccessibleMessage): message about a refunded payment, information about the payment. .. versionadded:: 21.4 + direct_message_price_changed (:class:`telegram.DirectMessagePriceChanged`): + Optional. Service message: the price for paid messages in the corresponding direct + messages chat of a channel has changed. + + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -983,10 +1018,14 @@ class Message(MaybeInaccessibleMessage): "channel_chat_created", "chat_background_set", "chat_shared", + "checklist", + "checklist_tasks_added", + "checklist_tasks_done", "connected_website", "contact", "delete_chat_photo", "dice", + "direct_message_price_changed", "document", "edit_date", "effect_id", @@ -1152,6 +1191,10 @@ def __init__( unique_gift: Optional[UniqueGiftInfo] = None, paid_message_price_changed: Optional[PaidMessagePriceChanged] = None, paid_star_count: Optional[int] = None, + direct_message_price_changed: Optional[DirectMessagePriceChanged] = None, + checklist: Optional[Checklist] = None, + checklist_tasks_done: Optional[ChecklistTasksDone] = None, + checklist_tasks_added: Optional[ChecklistTasksAdded] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1233,6 +1276,7 @@ def __init__( ) self.write_access_allowed: Optional[WriteAccessAllowed] = write_access_allowed self.has_media_spoiler: Optional[bool] = has_media_spoiler + self.checklist: Optional[Checklist] = checklist self.users_shared: Optional[UsersShared] = users_shared self.chat_shared: Optional[ChatShared] = chat_shared self.story: Optional[Story] = story @@ -1251,6 +1295,8 @@ def __init__( 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.checklist_tasks_done: Optional[ChecklistTasksDone] = checklist_tasks_done + self.checklist_tasks_added: Optional[ChecklistTasksAdded] = checklist_tasks_added self.effect_id: Optional[str] = effect_id self.show_caption_above_media: Optional[bool] = show_caption_above_media self.paid_media: Optional[PaidMediaInfo] = paid_media @@ -1261,6 +1307,9 @@ def __init__( paid_message_price_changed ) self.paid_star_count: Optional[int] = paid_star_count + self.direct_message_price_changed: Optional[DirectMessagePriceChanged] = ( + direct_message_price_changed + ) self._effective_attachment = DEFAULT_NONE @@ -1437,6 +1486,16 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Message": data["reply_to_story"] = de_json_optional(data.get("reply_to_story"), Story, bot) data["boost_added"] = de_json_optional(data.get("boost_added"), ChatBoostAdded, bot) data["sender_business_bot"] = de_json_optional(data.get("sender_business_bot"), User, bot) + data["direct_message_price_changed"] = de_json_optional( + data.get("direct_message_price_changed"), DirectMessagePriceChanged, bot + ) + data["checklist"] = de_json_optional(data.get("checklist"), Checklist, bot) + data["checklist_tasks_done"] = de_json_optional( + data.get("checklist_tasks_done"), ChecklistTasksDone, bot + ) + data["checklist_tasks_added"] = de_json_optional( + data.get("checklist_tasks_added"), ChecklistTasksAdded, bot + ) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility @@ -3250,6 +3309,63 @@ async def reply_dice( allow_paid_broadcast=allow_paid_broadcast, ) + async def reply_checklist( + self, + checklist: InputChecklist, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_effect_id: Optional[str] = None, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_checklist( + business_connection_id=self.business_connection_id, + chat_id=update.effective_message.chat_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_checklist`. + + .. versionadded:: NEXT.VERSION + + Keyword Args: + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, reply_to_message_id, reply_parameters, allow_sending_without_reply + ) + return await self.get_bot().send_checklist( + business_connection_id=self.business_connection_id, + chat_id=chat_id, # type: ignore[arg-type] + checklist=checklist, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_effect_id=message_effect_id, + ) + async def reply_chat_action( self, action: str, @@ -3859,6 +3975,53 @@ async def edit_caption( business_connection_id=self.business_connection_id, ) + async def edit_checklist( + self, + checklist: InputChecklist, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.edit_message_checklist( + business_connection_id=message.business_connection_id, + chat_id=message.chat_id, + message_id=message.message_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_checklist`. + + .. versionadded:: NEXT.VERSION + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + :class:`telegram.Message`: On success, the edited Message is returned. + + """ + return await self.get_bot().edit_message_checklist( + business_connection_id=self.business_connection_id, + chat_id=self.chat_id, + message_id=self.message_id, + checklist=checklist, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def edit_media( self, media: "InputMedia", diff --git a/src/telegram/_ownedgift.py b/src/telegram/_ownedgift.py index 875a01540f1..8efdccfe111 100644 --- a/src/telegram/_ownedgift.py +++ b/src/telegram/_ownedgift.py @@ -347,13 +347,17 @@ class OwnedGiftUnique(OwnedGift): bot; for gifts received on behalf of business accounts only. sender_user (:class:`telegram.User`, optional): Sender of the gift if it is a known user. send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`. - |datetime_localization|. + |datetime_localization| is_saved (:obj:`bool`, optional): :obj:`True`, if the gift is displayed on the account's profile page; for gifts received on behalf of business accounts only. can_be_transferred (:obj:`bool`, optional): :obj:`True`, if the gift can be transferred to another owner; for gifts received on behalf of business accounts only. transfer_star_count (:obj:`int`, optional): Number of Telegram Stars that must be paid to transfer the gift; omitted if the bot cannot transfer the gift. + next_transfer_date (:obj:`datetime.datetime`, optional): Date when the gift can be + transferred. If it's in the past, then the gift can be transferred now. + |datetime_localization| + .. versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): Type of the owned gift, always :tg-const:`~telegram.OwnedGift.UNIQUE`. @@ -362,19 +366,24 @@ class OwnedGiftUnique(OwnedGift): bot; for gifts received on behalf of business accounts only. sender_user (:class:`telegram.User`): Optional. Sender of the gift if it is a known user. send_date (:obj:`datetime.datetime`): Date the gift was sent as :class:`datetime.datetime`. - |datetime_localization|. + |datetime_localization| is_saved (:obj:`bool`): Optional. :obj:`True`, if the gift is displayed on the account's profile page; for gifts received on behalf of business accounts only. can_be_transferred (:obj:`bool`): Optional. :obj:`True`, if the gift can be transferred to another owner; for gifts received on behalf of business accounts only. transfer_star_count (:obj:`int`): Optional. Number of Telegram Stars that must be paid to transfer the gift; omitted if the bot cannot transfer the gift. + next_transfer_date (:obj:`datetime.datetime`): Optional. Date when the gift can be + transferred. If it's in the past, then the gift can be transferred now. + |datetime_localization| + .. versionadded:: NEXT.VERSION """ __slots__ = ( "can_be_transferred", "gift", "is_saved", + "next_transfer_date", "owned_gift_id", "send_date", "sender_user", @@ -390,6 +399,7 @@ def __init__( is_saved: Optional[bool] = None, can_be_transferred: Optional[bool] = None, transfer_star_count: Optional[int] = None, + next_transfer_date: Optional[dtm.datetime] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: @@ -403,6 +413,7 @@ def __init__( self.is_saved: Optional[bool] = is_saved self.can_be_transferred: Optional[bool] = can_be_transferred self.transfer_star_count: Optional[int] = transfer_star_count + self.next_transfer_date: Optional[dtm.datetime] = next_transfer_date self._id_attrs = (self.type, self.gift, self.send_date) @@ -415,5 +426,8 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "OwnedGiftUniqu data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot) data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot) + data["next_transfer_date"] = from_timestamp( + data.get("next_transfer_date"), tzinfo=loc_tzinfo + ) return super().de_json(data=data, bot=bot) # type: ignore[return-value] diff --git a/src/telegram/_reply.py b/src/telegram/_reply.py index ca6b23b0507..97c88215869 100644 --- a/src/telegram/_reply.py +++ b/src/telegram/_reply.py @@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Optional, Union from telegram._chat import Chat +from telegram._checklists import Checklist from telegram._dice import Dice from telegram._files.animation import Animation from telegram._files.audio import Audio @@ -89,6 +90,9 @@ class ExternalReplyInfo(TelegramObject): the file. has_media_spoiler (:obj:`bool`, optional): :obj:`True`, if the message media is covered by a spoiler animation. + checklist (:class:`telegram.Checklist`, optional): Message is a checklist + + .. versionadded:: NEXT.VERSION contact (:class:`telegram.Contact`, optional): Message is a shared contact, information about the contact. dice (:class:`telegram.Dice`, optional): Message is a dice with random value. @@ -138,6 +142,9 @@ class ExternalReplyInfo(TelegramObject): the file. has_media_spoiler (:obj:`bool`): Optional. :obj:`True`, if the message media is covered by a spoiler animation. + checklist (:class:`telegram.Checklist`): Optional. Message is a checklist + + .. versionadded:: NEXT.VERSION contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information about the contact. dice (:class:`telegram.Dice`): Optional. Message is a dice with random value. @@ -164,6 +171,7 @@ class ExternalReplyInfo(TelegramObject): "animation", "audio", "chat", + "checklist", "contact", "dice", "document", @@ -213,6 +221,7 @@ def __init__( poll: Optional[Poll] = None, venue: Optional[Venue] = None, paid_media: Optional[PaidMediaInfo] = None, + checklist: Optional[Checklist] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -232,6 +241,7 @@ def __init__( self.video_note: Optional[VideoNote] = video_note self.voice: Optional[Voice] = voice self.has_media_spoiler: Optional[bool] = has_media_spoiler + self.checklist: Optional[Checklist] = checklist self.contact: Optional[Contact] = contact self.dice: Optional[Dice] = dice self.game: Optional[Game] = game @@ -278,6 +288,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ExternalReplyI data["poll"] = de_json_optional(data.get("poll"), Poll, bot) data["venue"] = de_json_optional(data.get("venue"), Venue, bot) data["paid_media"] = de_json_optional(data.get("paid_media"), PaidMediaInfo, bot) + data["checklist"] = de_json_optional(data.get("checklist"), Checklist, bot) return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_uniquegift.py b/src/telegram/_uniquegift.py index fa494a8e55a..9c2efae7faa 100644 --- a/src/telegram/_uniquegift.py +++ b/src/telegram/_uniquegift.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/] """This module contains classes related to unique gifs.""" +import datetime as dtm from typing import TYPE_CHECKING, Final, Optional from telegram import constants @@ -25,6 +26,7 @@ from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import de_json_optional +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -340,31 +342,63 @@ class UniqueGiftInfo(TelegramObject): Args: gift (:class:`UniqueGift`): Information about the gift. - origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` - or :attr:`TRANSFER`. + origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` for gifts + upgraded from regular gifts, :attr:`TRANSFER` for gifts transferred from other users + or channels, or :attr:`RESALE` for gifts bought from other users. + + .. versionchanged:: NEXT.VERSION + The :attr:`RESALE` origin was added. owned_gift_id (:obj:`str`, optional) Unique identifier of the received gift for the bot; only present for gifts received on behalf of business accounts. transfer_star_count (:obj:`int`, optional): Number of Telegram Stars that must be paid to transfer the gift; omitted if the bot cannot transfer the gift. + last_resale_star_count (:obj:`int`, optional): For gifts bought from other users, the price + paid for the gift. + + .. versionadded:: NEXT.VERSION + next_transfer_date (:obj:`datetime.datetime`, optional): Date when the gift can be + transferred. If it's in the past, then the gift can be transferred now. + |datetime_localization| + + .. versionadded:: NEXT.VERSION Attributes: gift (:class:`UniqueGift`): Information about the gift. - origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` - or :attr:`TRANSFER`. + origin (:obj:`str`): Origin of the gift. Currently, either :attr:`UPGRADE` for gifts + upgraded from regular gifts, :attr:`TRANSFER` for gifts transferred from other users + or channels, or :attr:`RESALE` for gifts bought from other users. + + .. versionchanged:: NEXT.VERSION + The :attr:`RESALE` origin was added. owned_gift_id (:obj:`str`) Optional. Unique identifier of the received gift for the bot; only present for gifts received on behalf of business accounts. transfer_star_count (:obj:`int`): Optional. Number of Telegram Stars that must be paid to transfer the gift; omitted if the bot cannot transfer the gift. + last_resale_star_count (:obj:`int`): Optional. For gifts bought from other users, the price + paid for the gift. + .. versionadded:: NEXT.VERSION + next_transfer_date (:obj:`datetime.datetime`): Optional. Date when the gift can be + transferred. If it's in the past, then the gift can be transferred now. + |datetime_localization| + + .. versionadded:: NEXT.VERSION """ UPGRADE: Final[str] = constants.UniqueGiftInfoOrigin.UPGRADE """:const:`telegram.constants.UniqueGiftInfoOrigin.UPGRADE`""" TRANSFER: Final[str] = constants.UniqueGiftInfoOrigin.TRANSFER """:const:`telegram.constants.UniqueGiftInfoOrigin.TRANSFER`""" + RESALE: Final[str] = constants.UniqueGiftInfoOrigin.RESALE + """:const:`telegram.constants.UniqueGiftInfoOrigin.RESALE` + + .. versionadded:: NEXT.VERSION + """ __slots__ = ( "gift", + "last_resale_star_count", + "next_transfer_date", "origin", "owned_gift_id", "transfer_star_count", @@ -376,6 +410,8 @@ def __init__( origin: str, owned_gift_id: Optional[str] = None, transfer_star_count: Optional[int] = None, + last_resale_star_count: Optional[int] = None, + next_transfer_date: Optional[dtm.datetime] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -386,6 +422,8 @@ def __init__( # Optional self.owned_gift_id: Optional[str] = owned_gift_id self.transfer_star_count: Optional[int] = transfer_star_count + self.last_resale_star_count: Optional[int] = last_resale_star_count + self.next_transfer_date: Optional[dtm.datetime] = next_transfer_date self._id_attrs = (self.gift, self.origin) @@ -396,6 +434,10 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UniqueGiftInfo """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) + loc_tzinfo = extract_tzinfo_from_defaults(bot) data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot) + data["next_transfer_date"] = from_timestamp( + data.get("next_transfer_date"), tzinfo=loc_tzinfo + ) return super().de_json(data=data, bot=bot) diff --git a/src/telegram/constants.py b/src/telegram/constants.py index c481de5e0ae..7cab2248713 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -77,6 +77,7 @@ "InlineQueryResultLimit", "InlineQueryResultType", "InlineQueryResultsButtonLimit", + "InputChecklistLimit", "InputMediaType", "InputPaidMediaType", "InputProfilePhotoType", @@ -172,7 +173,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=0) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=1) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -188,6 +189,7 @@ class _AccentColor(NamedTuple): #: :obj:`datetime.datetime`, value of unix 0. #: This date literal is used in :class:`telegram.InaccessibleMessage` +# and :class:`telegram.ChecklistTask`. #: #: .. versionadded:: 20.8 ZERO_DATE: Final[dtm.datetime] = dtm.datetime(1970, 1, 1, tzinfo=UTC) @@ -1411,6 +1413,47 @@ class InlineKeyboardMarkupLimit(IntEnum): """ +class InputChecklistLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InputChecklist`/ + :class:`telegram.InputChecklistTask`. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MIN_TITLE_LENGTH = 1 + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as + :paramref:`~telegram.InputChecklist.title` parameter of :class:`telegram.InputChecklist` + """ + + MAX_TITLE_LENGTH = 255 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as + :paramref:`~telegram.InputChecklist.title` parameter of :class:`telegram.InputChecklist` + """ + + MIN_TEXT_LENGTH = 1 + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as + :paramref:`~telegram.InputChecklistTask.text` parameter of :class:`telegram.InputChecklistTask` + """ + + MAX_TEXT_LENGTH = 100 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as + :paramref:`~telegram.InputChecklistTask.text` parameter of :class:`telegram.InputChecklistTask` + """ + + MIN_TASK_NUMBER = 1 + """:obj:`int`: Minimum number of tasks passed as :paramref:`~telegram.InputChecklist.tasks` + parameter of :class:`telegram.InputChecklist` + """ + + MAX_TASK_NUMBER = 30 + """:obj:`int`: Maximum number of tasks passed as :paramref:`~telegram.InputChecklistTask.tasks` + parameter of :class:`telegram.InputChecklistTask` + """ + + class InputMediaType(StringEnum): """This enum contains the available types of :class:`telegram.InputMedia`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. @@ -2058,6 +2101,21 @@ class MessageType(StringEnum): .. versionadded:: 21.2 """ + CHECKLIST = "checklist" + """:obj:`str`: Messages with :attr:`telegram.Message.checklist`. + + .. versionadded:: NEXT.VERSION + """ + CHECKLIST_TASKS_ADDED = "checklist_tasks_added" + """:obj:`str`: Messages with :attr:`telegram.Message.checklist_tasks_added`. + + .. versionadded:: NEXT.VERSION + """ + CHECKLIST_TASKS_DONE = "checklist_tasks_done" + """:obj:`str`: Messages with :attr:`telegram.Message.checklist_tasks_done`. + + .. versionadded:: NEXT.VERSION + """ CONNECTED_WEBSITE = "connected_website" """:obj:`str`: Messages with :attr:`telegram.Message.connected_website`.""" CONTACT = "contact" @@ -2066,6 +2124,11 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.delete_chat_photo`.""" DICE = "dice" """:obj:`str`: Messages with :attr:`telegram.Message.dice`.""" + DIRECT_MESSAGE_PRICE_CHANGED = "direct_message_price_changed" + """:obj:`str`: Messages with :attr:`telegram.Message.direct_message_price_changed`. + + .. versionadded:: NEXT.VERSION + """ DOCUMENT = "document" """:obj:`str`: Messages with :attr:`telegram.Message.document`.""" EFFECT_ID = "effect_id" @@ -3115,10 +3178,13 @@ class PollLimit(IntEnum): to the :paramref:`~telegram.Bot.send_poll.options` parameter of :meth:`telegram.Bot.send_poll`. """ - MAX_OPTION_NUMBER = 10 + MAX_OPTION_NUMBER = 12 """:obj:`int`: Maximum number of strings passed in a :obj:`list` to the :paramref:`~telegram.Bot.send_poll.options` parameter of :meth:`telegram.Bot.send_poll`. + + .. versionchanged:: NEXT.VERSION + This value was changed from ``10`` to ``12`` in accordance to Bot API 9.1. """ MAX_EXPLANATION_LENGTH = 200 """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the @@ -3173,6 +3239,11 @@ class UniqueGiftInfoOrigin(StringEnum): """:obj:`str` gift upgraded""" TRANSFER = "transfer" """:obj:`str` gift transfered""" + RESALE = "resale" + """:obj:`str` gift bought from other users + + .. versionadded:: NEXT.VERSION + """ class UpdateType(StringEnum): diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 5781cf817bc..3c956594632 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -62,6 +62,7 @@ Gifts, InlineKeyboardMarkup, InlineQueryResultsButton, + InputChecklist, InputMedia, InputPaidMedia, InputPollOption, @@ -2639,6 +2640,72 @@ async def send_contact( allow_paid_broadcast=allow_paid_broadcast, ) + async def send_checklist( + self, + business_connection_id: str, + chat_id: int, + checklist: InputChecklist, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_effect_id: Optional[str] = None, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> Message: + return await super().send_checklist( + business_connection_id=business_connection_id, + chat_id=chat_id, + checklist=checklist, + disable_notification=disable_notification, + protect_content=protect_content, + message_effect_id=message_effect_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def edit_message_checklist( + self, + business_connection_id: str, + chat_id: int, + message_id: int, + checklist: InputChecklist, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> Message: + return await super().edit_message_checklist( + business_connection_id=business_connection_id, + chat_id=chat_id, + message_id=message_id, + checklist=checklist, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def send_dice( self, chat_id: Union[int, str], @@ -5057,6 +5124,24 @@ async def remove_user_verification( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_my_star_balance( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> StarAmount: + return await super().get_my_star_balance( + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -5139,6 +5224,8 @@ async def remove_user_verification( setPassportDataErrors = set_passport_data_errors sendPoll = send_poll stopPoll = stop_poll + sendChecklist = send_checklist + editMessageChecklist = edit_message_checklist sendDice = send_dice getMyCommands = get_my_commands setMyCommands = set_my_commands @@ -5210,3 +5297,4 @@ async def remove_user_verification( verifyUser = verify_user removeChatVerification = remove_chat_verification removeUserVerification = remove_user_verification + getMyStarBalance = get_my_star_balance diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index f3272147442..6a5915063b4 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -46,6 +46,7 @@ "AUDIO", "BOOST_ADDED", "CAPTION", + "CHECKLIST", "COMMAND", "CONTACT", "EFFECT_ID", @@ -920,6 +921,20 @@ def filter(self, message: Message) -> bool: """Updates from supergroup.""" +class _Checklist(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.checklist) + + +CHECKLIST = _Checklist(name="filters.CHECKLIST") +"""Messages that contain :attr:`telegram.Message.checklist`. + +.. versionadded:: NEXT.VERSION +""" + + class Command(MessageFilter): """ Messages with a :attr:`telegram.MessageEntity.BOT_COMMAND`. By default, only allows @@ -1918,7 +1933,10 @@ def filter(self, update: Update) -> bool: StatusUpdate.CHAT_BACKGROUND_SET.check_update(update) or StatusUpdate.CHAT_CREATED.check_update(update) or StatusUpdate.CHAT_SHARED.check_update(update) + or StatusUpdate.CHECKLIST_TASKS_ADDED.check_update(update) + or StatusUpdate.CHECKLIST_TASKS_DONE.check_update(update) or StatusUpdate.CONNECTED_WEBSITE.check_update(update) + or StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED.check_update(update) or StatusUpdate.DELETE_CHAT_PHOTO.check_update(update) or StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) or StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) @@ -1988,6 +2006,30 @@ def filter(self, message: Message) -> bool: .. versionadded:: 20.1 """ + class _ChecklistTasksAdded(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.checklist_tasks_added) + + CHECKLIST_TASKS_ADDED = _ChecklistTasksAdded(name="filters.StatusUpdate.CHECKLIST_TASKS_ADDED") + """Messages that contain :attr:`telegram.Message.checklist_tasks_added`. + + .. versionadded:: NEXT.VERSION + """ + + class _ChecklistTasksDone(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.checklist_tasks_done) + + CHECKLIST_TASKS_DONE = _ChecklistTasksDone(name="filters.StatusUpdate.CHECKLIST_TASKS_DONE") + """Messages that contain :attr:`telegram.Message.checklist_tasks_done`. + + .. versionadded:: NEXT.VERSION + """ + class _ConnectedWebsite(MessageFilter): __slots__ = () @@ -1997,6 +2039,20 @@ def filter(self, message: Message) -> bool: CONNECTED_WEBSITE = _ConnectedWebsite(name="filters.StatusUpdate.CONNECTED_WEBSITE") """Messages that contain :attr:`telegram.Message.connected_website`.""" + class _DirectMessagePriceChanged(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.direct_message_price_changed) + + DIRECT_MESSAGE_PRICE_CHANGED = _DirectMessagePriceChanged( + name="filters.StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED" + ) + """Messages that contain :attr:`telegram.Message.direct_message_price_changed`. + + .. versionadded:: NEXT.VERSION + """ + class _DeleteChatPhoto(MessageFilter): __slots__ = () diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index 6802db2a206..097d6ddf706 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1116,7 +1116,22 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.PAID_MESSAGE_PRICE_CHANGED.check_update(update) update.message.paid_message_price_changed = None - def test_filters_forwarded(self, update, message_origin_user): + update.message.direct_message_price_changed = "direct_message_price_changed" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.DIRECT_MESSAGE_PRICE_CHANGED.check_update(update) + update.message.direct_message_price_changed = None + + update.message.checklist_tasks_added = "checklist_tasks_added" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.CHECKLIST_TASKS_ADDED.check_update(update) + update.message.checklist_tasks_added = None + + update.message.checklist_tasks_done = "checklist_tasks_done" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.CHECKLIST_TASKS_DONE.check_update(update) + update.message.checklist_tasks_done = None + + def test_filters_forwarded(self, update): assert filters.FORWARDED.check_update(update) update.message.forward_origin = MessageOriginHiddenUser(dtm.datetime.utcnow(), 1) assert filters.FORWARDED.check_update(update) @@ -2792,3 +2807,10 @@ def test_filters_sender_boost_count(self, update): update.message.sender_boost_count = "test" assert filters.SENDER_BOOST_COUNT.check_update(update) assert str(filters.SENDER_BOOST_COUNT) == "filters.SENDER_BOOST_COUNT" + + def test_filters_checklist(self, update): + assert not filters.CHECKLIST.check_update(update) + + update.message.checklist = "test" + assert filters.CHECKLIST.check_update(update) + assert str(filters.CHECKLIST) == "filters.CHECKLIST" diff --git a/tests/test_bot.py b/tests/test_bot.py index 4e78cd0a449..0e6e1355e3a 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -79,6 +79,7 @@ User, WebAppInfo, ) +from telegram._payment.stars.staramount import StarAmount from telegram._utils.datetime import UTC, from_timestamp, localize, to_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.strings import to_camel_case @@ -2574,6 +2575,17 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): await offline_bot.remove_chat_verification(1234) + async def test_get_my_star_balance(self, offline_bot, monkeypatch): + sa = StarAmount(1000).to_json() + + async def do_request(url, request_data: RequestData, *args, **kwargs): + assert not request_data.parameters + return 200, f'{{"ok": true, "result": {sa}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", do_request) + obj = await offline_bot.get_my_star_balance() + assert isinstance(obj, StarAmount) + class TestBotWithRequest: """ @@ -4540,3 +4552,8 @@ async def test_create_edit_chat_subscription_link( assert edited_link.name == "sub_name_2" assert sub_link.subscription_period == 2592000 assert sub_link.subscription_price == 13 + + async def test_get_my_star_balance(self, bot): + balance = await bot.get_my_star_balance() + assert isinstance(balance, StarAmount) + assert balance.amount == 0 diff --git a/tests/test_business_methods.py b/tests/test_business_methods.py index bdbc2e2d9b0..4e44a84e37d 100644 --- a/tests/test_business_methods.py +++ b/tests/test_business_methods.py @@ -36,7 +36,12 @@ from telegram._files._inputstorycontent import InputStoryContentVideo from telegram._files.sticker import Sticker from telegram._gifts import AcceptedGiftTypes, Gift +from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inputchecklist import InputChecklist, InputChecklistTask +from telegram._message import Message from telegram._ownedgift import OwnedGiftRegular, OwnedGifts +from telegram._reply import ReplyParameters from telegram._utils.datetime import UTC from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.constants import InputProfilePhotoType, InputStoryContentType @@ -638,3 +643,149 @@ async def make_assertion(*args, **kwargs): monkeypatch.setattr(offline_bot.request, "post", make_assertion) assert await offline_bot.delete_story(business_connection_id=self.bci, story_id=story_id) + + async def test_send_checklist_all_args(self, offline_bot, monkeypatch): + chat_id = 123 + checklist = InputChecklist( + title="My Checklist", + tasks=[InputChecklistTask(1, "Task 1"), InputChecklistTask(2, "Task 2")], + ) + disable_notification = True + protect_content = False + message_effect_id = 42 + reply_parameters = ReplyParameters(23, chat_id, allow_sending_without_reply=True) + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="test", callback_data="test2")]] + ) + json_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_json() + + async def make_assertions(*args, **kwargs): + params = kwargs.get("request_data").parameters + assert params.get("business_connection_id") == self.bci + assert params.get("chat_id") == chat_id + assert params.get("checklist") == checklist.to_dict() + assert params.get("disable_notification") is disable_notification + assert params.get("protect_content") is protect_content + assert params.get("message_effect_id") == message_effect_id + assert params.get("reply_parameters") == reply_parameters.to_dict() + assert params.get("reply_markup") == reply_markup.to_dict() + + return 200, f'{{"ok": true, "result": {json_message}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", make_assertions) + obj = await offline_bot.send_checklist( + business_connection_id=self.bci, + chat_id=chat_id, + checklist=checklist, + disable_notification=disable_notification, + protect_content=protect_content, + message_effect_id=message_effect_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + ) + assert isinstance(obj, Message) + + @pytest.mark.parametrize("default_bot", [{"disable_notification": True}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, True), (False, False), (None, None)], + ) + async def test_send_checklist_default_disable_notification( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("disable_notification") is expected_value + return Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_dict() + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "chat_id": 123, + "checklist": InputChecklist( + title="My Checklist", + tasks=[InputChecklistTask(1, "Task 1")], + ), + } + if passed_value is not DEFAULT_NONE: + kwargs["disable_notification"] = passed_value + + await default_bot.send_checklist(**kwargs) + + @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, True), (False, False), (None, None)], + ) + async def test_send_checklist_default_protect_content( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("protect_content") is expected_value + return Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_dict() + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "chat_id": 123, + "checklist": InputChecklist( + title="My Checklist", + tasks=[InputChecklistTask(1, "Task 1")], + ), + } + if passed_value is not DEFAULT_NONE: + kwargs["protect_content"] = passed_value + + await default_bot.send_checklist(**kwargs) + + async def test_send_checklist_mutually_exclusive_reply_parameters(self, offline_bot): + """Test that reply_to_message_id and allow_sending_without_reply are mutually exclusive + with reply_parameters.""" + with pytest.raises(ValueError, match="`reply_to_message_id` and"): + await offline_bot.send_checklist( + self.bci, + 123, + InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]), + reply_to_message_id=1, + reply_parameters=True, + ) + + with pytest.raises(ValueError, match="`allow_sending_without_reply` and"): + await offline_bot.send_checklist( + self.bci, + 123, + InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]), + allow_sending_without_reply=True, + reply_parameters=True, + ) + + async def test_edit_message_checklist_all_args(self, offline_bot, monkeypatch): + chat_id = 123 + message_id = 45 + checklist = InputChecklist( + title="My Checklist", + tasks=[InputChecklistTask(1, "Task 1"), InputChecklistTask(2, "Task 2")], + ) + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="test", callback_data="test2")]] + ) + json_message = Message(1, dtm.datetime.now(), Chat(1, ""), text="test").to_json() + + async def make_assertions(*args, **kwargs): + params = kwargs.get("request_data").parameters + assert params.get("business_connection_id") == self.bci + assert params.get("chat_id") == chat_id + assert params.get("message_id") == message_id + assert params.get("checklist") == checklist.to_dict() + assert params.get("reply_markup") == reply_markup.to_dict() + + return 200, f'{{"ok": true, "result": {json_message}}}'.encode() + + monkeypatch.setattr(offline_bot.request, "do_request", make_assertions) + obj = await offline_bot.edit_message_checklist( + business_connection_id=self.bci, + chat_id=chat_id, + message_id=message_id, + checklist=checklist, + reply_markup=reply_markup, + ) + assert isinstance(obj, Message) diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 97ef9b2a627..6b759e885cb 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -21,7 +21,17 @@ import pytest -from telegram import Audio, Bot, CallbackQuery, Chat, InaccessibleMessage, Message, User +from telegram import ( + Audio, + Bot, + CallbackQuery, + Chat, + InaccessibleMessage, + InputChecklist, + InputChecklistTask, + Message, + User, +) from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, @@ -230,6 +240,43 @@ async def make_assertion(*_, **kwargs): assert await callback_query.edit_message_caption(caption="new caption") assert await callback_query.edit_message_caption("new caption") + async def test_edit_message_checklist(self, monkeypatch, callback_query): + checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]) + + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.edit_message_checklist(checklist) + return + + if callback_query.inline_message_id: + pytest.skip("Can't edit inline messages") + + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == callback_query.message.chat_id + message_id = kwargs["message_id"] == callback_query.message.message_id + caption = kwargs["checklist"] == checklist + return chat_id and message_id and caption + + assert check_shortcut_signature( + CallbackQuery.edit_message_checklist, + Bot.edit_message_checklist, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.edit_message_checklist, + callback_query.get_bot(), + "edit_message_checklist", + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], + ) + assert await check_defaults_handling( + callback_query.edit_message_checklist, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "edit_message_checklist", make_assertion) + assert await callback_query.edit_message_checklist(checklist=checklist) + assert await callback_query.edit_message_checklist(checklist) + async def test_edit_message_reply_markup(self, monkeypatch, callback_query): if isinstance(callback_query.message, InaccessibleMessage): with pytest.raises(TypeError, match="inaccessible message"): diff --git a/tests/test_chat.py b/tests/test_chat.py index 8e901fb91bf..4651393e473 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -20,7 +20,15 @@ import pytest -from telegram import Bot, Chat, ChatPermissions, ReactionTypeEmoji, User +from telegram import ( + Bot, + Chat, + ChatPermissions, + InputChecklist, + InputChecklistTask, + ReactionTypeEmoji, + User, +) from telegram.constants import ChatAction, ChatType, ReactionEmoji from telegram.helpers import escape_markdown from tests.auxil.bot_method_checks import ( @@ -579,6 +587,23 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "send_dice", make_assertion) assert await chat.send_dice(emoji="test_dice") + async def test_instance_method_send_checklist(self, monkeypatch, chat): + checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]) + + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["business_connection_id"] == "123" + and kwargs["checklist"] == checklist + ) + + assert check_shortcut_signature(Chat.send_checklist, Bot.send_checklist, ["chat_id"], []) + assert await check_shortcut_call(chat.send_checklist, chat.get_bot(), "send_checklist") + assert await check_defaults_handling(chat.send_checklist, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_checklist", make_assertion) + assert await chat.send_checklist("123", checklist) + async def test_instance_method_send_game(self, monkeypatch, chat): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == chat.id and kwargs["game_short_name"] == "test_game" diff --git a/tests/test_checklists.py b/tests/test_checklists.py new file mode 100644 index 00000000000..dda79105437 --- /dev/null +++ b/tests/test_checklists.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import datetime as dtm + +import pytest + +from telegram import ( + Checklist, + ChecklistTask, + ChecklistTasksAdded, + ChecklistTasksDone, + Dice, + MessageEntity, + User, +) +from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import ZERO_DATE +from tests.auxil.build_messages import make_message +from tests.auxil.slots import mro_slots + + +class ChecklistTaskTestBase: + id = 42 + text = "here is a text" + text_entities = [ + MessageEntity(type="bold", offset=0, length=4), + MessageEntity(type="italic", offset=5, length=2), + ] + completed_by_user = User(id=1, first_name="Test", last_name="User", is_bot=False) + completion_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) + + +@pytest.fixture(scope="module") +def checklist_task(): + return ChecklistTask( + id=ChecklistTaskTestBase.id, + text=ChecklistTaskTestBase.text, + text_entities=ChecklistTaskTestBase.text_entities, + completed_by_user=ChecklistTaskTestBase.completed_by_user, + completion_date=ChecklistTaskTestBase.completion_date, + ) + + +class TestChecklistTaskWithoutRequest(ChecklistTaskTestBase): + def test_slot_behaviour(self, checklist_task): + for attr in checklist_task.__slots__: + assert getattr(checklist_task, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(checklist_task)) == len( + set(mro_slots(checklist_task)) + ), "duplicate slot" + + def test_to_dict(self, checklist_task): + clt_dict = checklist_task.to_dict() + assert isinstance(clt_dict, dict) + assert clt_dict["id"] == self.id + assert clt_dict["text"] == self.text + assert clt_dict["text_entities"] == [entity.to_dict() for entity in self.text_entities] + assert clt_dict["completed_by_user"] == self.completed_by_user.to_dict() + assert clt_dict["completion_date"] == to_timestamp(self.completion_date) + + def test_de_json(self, offline_bot): + json_dict = { + "id": self.id, + "text": self.text, + "text_entities": [entity.to_dict() for entity in self.text_entities], + "completed_by_user": self.completed_by_user.to_dict(), + "completion_date": to_timestamp(self.completion_date), + } + clt = ChecklistTask.de_json(json_dict, offline_bot) + assert isinstance(clt, ChecklistTask) + assert clt.id == self.id + assert clt.text == self.text + assert clt.text_entities == tuple(self.text_entities) + assert clt.completed_by_user == self.completed_by_user + assert clt.completion_date == self.completion_date + assert clt.api_kwargs == {} + + def test_de_json_required_fields(self, offline_bot): + json_dict = { + "id": self.id, + "text": self.text, + } + clt = ChecklistTask.de_json(json_dict, offline_bot) + assert isinstance(clt, ChecklistTask) + assert clt.id == self.id + assert clt.text == self.text + assert clt.text_entities == () + assert clt.completed_by_user is None + assert clt.completion_date is None + assert clt.api_kwargs == {} + + def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): + json_dict = { + "id": self.id, + "text": self.text, + "completion_date": to_timestamp(self.completion_date), + } + clt_bot = ChecklistTask.de_json(json_dict, offline_bot) + clt_bot_raw = ChecklistTask.de_json(json_dict, raw_bot) + clt_bot_tz = ChecklistTask.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + completion_date_offset = clt_bot_tz.completion_date.utcoffset() + completion_date_offset_tz = tz_bot.defaults.tzinfo.utcoffset( + clt_bot_tz.completion_date.replace(tzinfo=None) + ) + + assert clt_bot.completion_date.tzinfo == UTC + assert clt_bot_raw.completion_date.tzinfo == UTC + assert completion_date_offset_tz == completion_date_offset + + @pytest.mark.parametrize( + ("completion_date", "expected"), + [ + (None, None), + (0, ZERO_DATE), + (1735689600, dtm.datetime(2025, 1, 1, tzinfo=UTC)), + ], + ) + def test_de_json_completion_date(self, offline_bot, completion_date, expected): + json_dict = { + "id": self.id, + "text": self.text, + "completion_date": completion_date, + } + clt = ChecklistTask.de_json(json_dict, offline_bot) + assert isinstance(clt, ChecklistTask) + assert clt.completion_date == expected + + def test_parse_entity(self, checklist_task): + assert checklist_task.parse_entity(checklist_task.text_entities[0]) == "here" + + def test_parse_entities(self, checklist_task): + assert checklist_task.parse_entities(MessageEntity.BOLD) == { + checklist_task.text_entities[0]: "here" + } + assert checklist_task.parse_entities() == { + checklist_task.text_entities[0]: "here", + checklist_task.text_entities[1]: "is", + } + + def test_equality(self, checklist_task): + clt1 = checklist_task + clt2 = ChecklistTask( + id=self.id, + text="other text", + ) + clt3 = ChecklistTask( + id=self.id + 1, + text=self.text, + ) + clt4 = Dice(value=1, emoji="🎲") + + assert clt1 == clt2 + assert hash(clt1) == hash(clt2) + + assert clt1 != clt3 + assert hash(clt1) != hash(clt3) + + assert clt1 != clt4 + assert hash(clt1) != hash(clt4) + + +class ChecklistTestBase: + title = "Checklist Title" + title_entities = [ + MessageEntity(type="bold", offset=0, length=9), + MessageEntity(type="italic", offset=10, length=5), + ] + tasks = [ + ChecklistTask( + id=1, + text="Task 1", + ), + ChecklistTask( + id=2, + text="Task 2", + ), + ] + others_can_add_tasks = True + others_can_mark_tasks_as_done = False + + +@pytest.fixture(scope="module") +def checklist(): + return Checklist( + title=ChecklistTestBase.title, + title_entities=ChecklistTestBase.title_entities, + tasks=ChecklistTestBase.tasks, + others_can_add_tasks=ChecklistTestBase.others_can_add_tasks, + others_can_mark_tasks_as_done=ChecklistTestBase.others_can_mark_tasks_as_done, + ) + + +class TestChecklistWithoutRequest(ChecklistTestBase): + def test_slot_behaviour(self, checklist): + for attr in checklist.__slots__: + assert getattr(checklist, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(checklist)) == len(set(mro_slots(checklist))), "duplicate slot" + + def test_to_dict(self, checklist): + cl_dict = checklist.to_dict() + assert isinstance(cl_dict, dict) + assert cl_dict["title"] == self.title + assert cl_dict["title_entities"] == [entity.to_dict() for entity in self.title_entities] + assert cl_dict["tasks"] == [task.to_dict() for task in self.tasks] + assert cl_dict["others_can_add_tasks"] is self.others_can_add_tasks + assert cl_dict["others_can_mark_tasks_as_done"] is self.others_can_mark_tasks_as_done + + def test_de_json(self, offline_bot): + json_dict = { + "title": self.title, + "title_entities": [entity.to_dict() for entity in self.title_entities], + "tasks": [task.to_dict() for task in self.tasks], + "others_can_add_tasks": self.others_can_add_tasks, + "others_can_mark_tasks_as_done": self.others_can_mark_tasks_as_done, + } + cl = Checklist.de_json(json_dict, offline_bot) + assert isinstance(cl, Checklist) + assert cl.title == self.title + assert cl.title_entities == tuple(self.title_entities) + assert cl.tasks == tuple(self.tasks) + assert cl.others_can_add_tasks is self.others_can_add_tasks + assert cl.others_can_mark_tasks_as_done is self.others_can_mark_tasks_as_done + assert cl.api_kwargs == {} + + def test_de_json_required_fields(self, offline_bot): + json_dict = { + "title": self.title, + "tasks": [task.to_dict() for task in self.tasks], + } + cl = Checklist.de_json(json_dict, offline_bot) + assert isinstance(cl, Checklist) + assert cl.title == self.title + assert cl.title_entities == () + assert cl.tasks == tuple(self.tasks) + assert not cl.others_can_add_tasks + assert not cl.others_can_mark_tasks_as_done + + def test_parse_entity(self, checklist): + assert checklist.parse_entity(checklist.title_entities[0]) == "Checklist" + assert checklist.parse_entity(checklist.title_entities[1]) == "Title" + + def test_parse_entities(self, checklist): + assert checklist.parse_entities(MessageEntity.BOLD) == { + checklist.title_entities[0]: "Checklist" + } + assert checklist.parse_entities() == { + checklist.title_entities[0]: "Checklist", + checklist.title_entities[1]: "Title", + } + + def test_equality(self, checklist, checklist_task): + cl1 = checklist + cl2 = Checklist( + title=self.title + " other", + tasks=[ChecklistTask(id=1, text="something"), ChecklistTask(id=2, text="something")], + ) + cl3 = Checklist( + title=self.title + " other", + tasks=[ChecklistTask(id=42, text="Task 2")], + ) + cl4 = checklist_task + + assert cl1 == cl2 + assert hash(cl1) == hash(cl2) + + assert cl1 != cl3 + assert hash(cl1) != hash(cl3) + + assert cl1 != cl4 + assert hash(cl1) != hash(cl4) + + +class ChecklistTasksDoneTestBase: + checklist_message = make_message("Checklist message") + marked_as_done_task_ids = [1, 2, 3] + marked_as_not_done_task_ids = [4, 5] + + +@pytest.fixture(scope="module") +def checklist_tasks_done(): + return ChecklistTasksDone( + checklist_message=ChecklistTasksDoneTestBase.checklist_message, + marked_as_done_task_ids=ChecklistTasksDoneTestBase.marked_as_done_task_ids, + marked_as_not_done_task_ids=ChecklistTasksDoneTestBase.marked_as_not_done_task_ids, + ) + + +class TestChecklistTasksDoneWithoutRequest(ChecklistTasksDoneTestBase): + def test_slot_behaviour(self, checklist_tasks_done): + for attr in checklist_tasks_done.__slots__: + assert getattr(checklist_tasks_done, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(checklist_tasks_done)) == len( + set(mro_slots(checklist_tasks_done)) + ), "duplicate slot" + + def test_to_dict(self, checklist_tasks_done): + cltd_dict = checklist_tasks_done.to_dict() + assert isinstance(cltd_dict, dict) + assert cltd_dict["checklist_message"] == self.checklist_message.to_dict() + assert cltd_dict["marked_as_done_task_ids"] == self.marked_as_done_task_ids + assert cltd_dict["marked_as_not_done_task_ids"] == self.marked_as_not_done_task_ids + + def test_de_json(self, offline_bot): + json_dict = { + "checklist_message": self.checklist_message.to_dict(), + "marked_as_done_task_ids": self.marked_as_done_task_ids, + "marked_as_not_done_task_ids": self.marked_as_not_done_task_ids, + } + cltd = ChecklistTasksDone.de_json(json_dict, offline_bot) + assert isinstance(cltd, ChecklistTasksDone) + assert cltd.checklist_message == self.checklist_message + assert cltd.marked_as_done_task_ids == tuple(self.marked_as_done_task_ids) + assert cltd.marked_as_not_done_task_ids == tuple(self.marked_as_not_done_task_ids) + assert cltd.api_kwargs == {} + + def test_de_json_required_fields(self, offline_bot): + cltd = ChecklistTasksDone.de_json({}, offline_bot) + assert isinstance(cltd, ChecklistTasksDone) + assert cltd.checklist_message is None + assert cltd.marked_as_done_task_ids == () + assert cltd.marked_as_not_done_task_ids == () + assert cltd.api_kwargs == {} + + def test_equality(self, checklist_tasks_done): + cltd1 = checklist_tasks_done + cltd2 = ChecklistTasksDone( + checklist_message=None, + marked_as_done_task_ids=[1, 2, 3], + marked_as_not_done_task_ids=[4, 5], + ) + cltd3 = ChecklistTasksDone( + checklist_message=make_message("Checklist message"), + marked_as_done_task_ids=[1, 2, 3], + ) + cltd4 = make_message("Not a checklist tasks done") + + assert cltd1 == cltd2 + assert hash(cltd1) == hash(cltd2) + + assert cltd1 != cltd3 + assert hash(cltd1) != hash(cltd3) + + assert cltd1 != cltd4 + assert hash(cltd1) != hash(cltd4) + + +class ChecklistTasksAddedTestBase: + checklist_message = make_message("Checklist message") + tasks = [ + ChecklistTask(id=1, text="Task 1"), + ChecklistTask(id=2, text="Task 2"), + ChecklistTask(id=3, text="Task 3"), + ] + + +@pytest.fixture(scope="module") +def checklist_tasks_added(): + return ChecklistTasksAdded( + checklist_message=ChecklistTasksAddedTestBase.checklist_message, + tasks=ChecklistTasksAddedTestBase.tasks, + ) + + +class TestChecklistTasksAddedWithoutRequest(ChecklistTasksAddedTestBase): + def test_slot_behaviour(self, checklist_tasks_added): + for attr in checklist_tasks_added.__slots__: + assert getattr(checklist_tasks_added, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(checklist_tasks_added)) == len( + set(mro_slots(checklist_tasks_added)) + ), "duplicate slot" + + def test_to_dict(self, checklist_tasks_added): + clta_dict = checklist_tasks_added.to_dict() + assert isinstance(clta_dict, dict) + assert clta_dict["checklist_message"] == self.checklist_message.to_dict() + assert clta_dict["tasks"] == [task.to_dict() for task in self.tasks] + + def test_de_json(self, offline_bot): + json_dict = { + "checklist_message": self.checklist_message.to_dict(), + "tasks": [task.to_dict() for task in self.tasks], + } + clta = ChecklistTasksAdded.de_json(json_dict, offline_bot) + assert isinstance(clta, ChecklistTasksAdded) + assert clta.checklist_message == self.checklist_message + assert clta.tasks == tuple(self.tasks) + assert clta.api_kwargs == {} + + def test_de_json_required_fields(self, offline_bot): + clta = ChecklistTasksAdded.de_json( + {"tasks": [task.to_dict() for task in self.tasks]}, offline_bot + ) + assert isinstance(clta, ChecklistTasksAdded) + assert clta.checklist_message is None + assert clta.tasks == tuple(self.tasks) + assert clta.api_kwargs == {} + + def test_equality(self, checklist_tasks_added): + clta1 = checklist_tasks_added + clta2 = ChecklistTasksAdded( + checklist_message=None, + tasks=[ + ChecklistTask(id=1, text="Other Task 1"), + ChecklistTask(id=2, text="Other Task 2"), + ChecklistTask(id=3, text="Other Task 3"), + ], + ) + clta3 = ChecklistTasksAdded( + checklist_message=make_message("Checklist message"), + tasks=[ChecklistTask(id=1, text="Task 1")], + ) + clta4 = make_message("Not a checklist tasks added") + + assert clta1 == clta2 + assert hash(clta1) == hash(clta2) + + assert clta1 != clta3 + assert hash(clta1) != hash(clta3) + + assert clta1 != clta4 + assert hash(clta1) != hash(clta4) diff --git a/tests/test_directmessagepricechanged.py b/tests/test_directmessagepricechanged.py new file mode 100644 index 00000000000..39d831bcfb6 --- /dev/null +++ b/tests/test_directmessagepricechanged.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object for testing a Direct Message Price.""" + +from typing import TYPE_CHECKING + +import pytest + +from telegram import DirectMessagePriceChanged, User +from tests.auxil.slots import mro_slots + +if TYPE_CHECKING: + from telegram._utils.types import JSONDict + + +@pytest.fixture +def direct_message_price_changed(): + return DirectMessagePriceChanged( + are_direct_messages_enabled=DirectMessagePriceChangedTestBase.are_direct_messages_enabled, + direct_message_star_count=DirectMessagePriceChangedTestBase.direct_message_star_count, + ) + + +class DirectMessagePriceChangedTestBase: + are_direct_messages_enabled: bool = True + direct_message_star_count: int = 100 + + +class TestDirectMessagePriceChangedWithoutRequest(DirectMessagePriceChangedTestBase): + def test_slot_behaviour(self, direct_message_price_changed): + action = direct_message_price_changed + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self, offline_bot): + json_dict: JSONDict = { + "are_direct_messages_enabled": self.are_direct_messages_enabled, + "direct_message_star_count": self.direct_message_star_count, + } + dmpc = DirectMessagePriceChanged.de_json(json_dict, offline_bot) + assert dmpc.api_kwargs == {} + + assert dmpc.are_direct_messages_enabled == self.are_direct_messages_enabled + assert dmpc.direct_message_star_count == self.direct_message_star_count + + def test_to_dict(self, direct_message_price_changed): + dmpc_dict = direct_message_price_changed.to_dict() + assert dmpc_dict["are_direct_messages_enabled"] == self.are_direct_messages_enabled + assert dmpc_dict["direct_message_star_count"] == self.direct_message_star_count + + def test_equality(self, direct_message_price_changed): + dmpc1 = direct_message_price_changed + dmpc2 = DirectMessagePriceChanged( + are_direct_messages_enabled=self.are_direct_messages_enabled, + direct_message_star_count=self.direct_message_star_count, + ) + assert dmpc1 == dmpc2 + assert hash(dmpc1) == hash(dmpc2) + + dmpc3 = DirectMessagePriceChanged( + are_direct_messages_enabled=False, + direct_message_star_count=self.direct_message_star_count, + ) + assert dmpc1 != dmpc3 + assert hash(dmpc1) != hash(dmpc3) + + not_a_dmpc = User(id=1, first_name="wrong", is_bot=False) + assert dmpc1 != not_a_dmpc + assert hash(dmpc1) != hash(not_a_dmpc) diff --git a/tests/test_inputchecklist.py b/tests/test_inputchecklist.py new file mode 100644 index 00000000000..f21171eb5a3 --- /dev/null +++ b/tests/test_inputchecklist.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2025 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import pytest + +from telegram import Dice, InputChecklist, InputChecklistTask, MessageEntity +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def input_checklist_task(): + return InputChecklistTask( + id=InputChecklistTaskTestBase.id, + text=InputChecklistTaskTestBase.text, + parse_mode=InputChecklistTaskTestBase.parse_mode, + text_entities=InputChecklistTaskTestBase.text_entities, + ) + + +class InputChecklistTaskTestBase: + id = 1 + text = "buy food" + parse_mode = "MarkdownV2" + text_entities = [ + MessageEntity(type="bold", offset=0, length=3), + MessageEntity(type="italic", offset=4, length=4), + ] + + +class TestInputChecklistTaskWithoutRequest(InputChecklistTaskTestBase): + def test_slot_behaviour(self, input_checklist_task): + for attr in input_checklist_task.__slots__: + assert getattr(input_checklist_task, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(input_checklist_task)) == len( + set(mro_slots(input_checklist_task)) + ), "duplicate slot" + + def test_expected_values(self, input_checklist_task): + assert input_checklist_task.id == self.id + assert input_checklist_task.text == self.text + assert input_checklist_task.parse_mode == self.parse_mode + assert input_checklist_task.text_entities == tuple(self.text_entities) + + def test_to_dict(self, input_checklist_task): + iclt_dict = input_checklist_task.to_dict() + + assert isinstance(iclt_dict, dict) + assert iclt_dict["id"] == self.id + assert iclt_dict["text"] == self.text + assert iclt_dict["parse_mode"] == self.parse_mode + assert iclt_dict["text_entities"] == [entity.to_dict() for entity in self.text_entities] + + # Test that default-value parameter `parse_mode` is handled correctly + input_checklist_task = InputChecklistTask(id=1, text="text") + iclt_dict = input_checklist_task.to_dict() + assert "parse_mode" not in iclt_dict + + def test_equality(self, input_checklist_task): + a = input_checklist_task + b = InputChecklistTask(id=self.id, text=f"other {self.text}") + c = InputChecklistTask(id=self.id + 1, text=self.text) + d = Dice(value=1, emoji="🎲") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture(scope="module") +def input_checklist(): + return InputChecklist( + title=InputChecklistTestBase.title, + tasks=InputChecklistTestBase.tasks, + parse_mode=InputChecklistTestBase.parse_mode, + title_entities=InputChecklistTestBase.title_entities, + others_can_add_tasks=InputChecklistTestBase.others_can_add_tasks, + others_can_mark_tasks_as_done=InputChecklistTestBase.others_can_mark_tasks_as_done, + ) + + +class InputChecklistTestBase: + title = "test list" + tasks = [ + InputChecklistTask(id=1, text="eat"), + InputChecklistTask(id=2, text="sleep"), + ] + parse_mode = "MarkdownV2" + title_entities = [ + MessageEntity(type="bold", offset=0, length=4), + MessageEntity(type="italic", offset=5, length=4), + ] + others_can_add_tasks = True + others_can_mark_tasks_as_done = False + + +class TestInputChecklistWithoutRequest(InputChecklistTestBase): + def test_slot_behaviour(self, input_checklist): + for attr in input_checklist.__slots__: + assert getattr(input_checklist, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(input_checklist)) == len( + set(mro_slots(input_checklist)) + ), "duplicate slot" + + def test_expected_values(self, input_checklist): + assert input_checklist.title == self.title + assert input_checklist.tasks == tuple(self.tasks) + assert input_checklist.parse_mode == self.parse_mode + assert input_checklist.title_entities == tuple(self.title_entities) + assert input_checklist.others_can_add_tasks == self.others_can_add_tasks + assert input_checklist.others_can_mark_tasks_as_done == self.others_can_mark_tasks_as_done + + def test_to_dict(self, input_checklist): + icl_dict = input_checklist.to_dict() + + assert isinstance(icl_dict, dict) + assert icl_dict["title"] == self.title + assert icl_dict["tasks"] == [task.to_dict() for task in self.tasks] + assert icl_dict["parse_mode"] == self.parse_mode + assert icl_dict["title_entities"] == [entity.to_dict() for entity in self.title_entities] + assert icl_dict["others_can_add_tasks"] == self.others_can_add_tasks + assert icl_dict["others_can_mark_tasks_as_done"] == self.others_can_mark_tasks_as_done + + # Test that default-value parameter `parse_mode` is handled correctly + input_checklist = InputChecklist(title=self.title, tasks=self.tasks) + icl_dict = input_checklist.to_dict() + assert "parse_mode" not in icl_dict + + def test_equality(self, input_checklist): + a = input_checklist + b = InputChecklist( + title=f"other {self.title}", + tasks=[InputChecklistTask(id=1, text="eat"), InputChecklistTask(id=2, text="sleep")], + ) + c = InputChecklist( + title=self.title, + tasks=[InputChecklistTask(id=9, text="Other Task")], + ) + d = Dice(value=1, emoji="🎲") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_message.py b/tests/test_message.py index 1c5cd152859..ba8fa439f4d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -31,15 +31,24 @@ ChatBackground, ChatBoostAdded, ChatShared, + Checklist, + ChecklistTask, + ChecklistTasksAdded, + ChecklistTasksDone, Contact, Dice, + DirectMessagePriceChanged, Document, ExternalReplyInfo, Game, + Gift, + GiftInfo, Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners, + InputChecklist, + InputChecklistTask, InputPaidMediaPhoto, Invoice, LinkPreviewOptions, @@ -63,6 +72,12 @@ Story, SuccessfulPayment, TextQuote, + UniqueGift, + UniqueGiftBackdrop, + UniqueGiftBackdropColors, + UniqueGiftInfo, + UniqueGiftModel, + UniqueGiftSymbol, Update, User, UsersShared, @@ -76,15 +91,6 @@ Voice, WebAppData, ) -from telegram._gifts import Gift, GiftInfo -from telegram._uniquegift import ( - UniqueGift, - UniqueGiftBackdrop, - UniqueGiftBackdropColors, - UniqueGiftInfo, - UniqueGiftModel, - UniqueGiftSymbol, -) from telegram._utils.datetime import UTC from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput @@ -331,6 +337,24 @@ def message(bot): {"refunded_payment": RefundedPayment("EUR", 243, "payload", "charge_id", "provider_id")}, {"paid_star_count": 291}, {"paid_message_price_changed": PaidMessagePriceChanged(291)}, + {"direct_message_price_changed": DirectMessagePriceChanged(True, 100)}, + { + "checklist": Checklist( + "checklist_id", + tasks=[ChecklistTask(id=42, text="task 1"), ChecklistTask(id=43, text="task 2")], + ) + }, + { + "checklist_tasks_done": ChecklistTasksDone( + marked_as_done_task_ids=[1, 2, 3], + marked_as_not_done_task_ids=[4, 5], + ) + }, + { + "checklist_tasks_added": ChecklistTasksAdded( + tasks=[ChecklistTask(id=42, text="task 1"), ChecklistTask(id=43, text="task 2")], + ) + }, ], ids=[ "reply", @@ -408,6 +432,10 @@ def message(bot): "refunded_payment", "paid_star_count", "paid_message_price_changed", + "direct_message_price_changed", + "checklist", + "checklist_tasks_done", + "checklist_tasks_added", ], ) def message_params(bot, request): @@ -2196,6 +2224,42 @@ async def make_assertion(*_, **kwargs): message, message.reply_dice, "send_dice", [], monkeypatch ) + async def test_reply_checklist(self, monkeypatch, message): + checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]) + + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["business_connection_id"] == message.business_connection_id + and kwargs["checklist"] == checklist + and kwargs["disable_notification"] is True + ) + + assert check_shortcut_signature( + Message.reply_checklist, + Bot.send_checklist, + ["chat_id", "business_connection_id", "reply_to_message_id"], + ["do_quote", "reply_to_message_id"], + ) + assert await check_shortcut_call( + message.reply_checklist, + message.get_bot(), + "send_checklist", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["chat_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.reply_checklist, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "send_checklist", make_assertion) + assert await message.reply_checklist(checklist, disable_notification=True) + await self.check_quote_parsing( + message, + message.reply_checklist, + "send_checklist", + [checklist, True], + monkeypatch, + ) + async def test_reply_action(self, monkeypatch, message: Message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id @@ -2531,6 +2595,34 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(message.get_bot(), "edit_message_caption", make_assertion) assert await message.edit_caption(caption="new caption") + async def test_edit_checklist(self, monkeypatch, message): + checklist = InputChecklist(title="My Checklist", tasks=[InputChecklistTask(1, "Task 1")]) + + async def make_assertion(*_, **kwargs): + return ( + kwargs["business_connection_id"] == message.business_connection_id + and kwargs["chat_id"] == message.chat_id + and kwargs["message_id"] == message.message_id + and kwargs["checklist"] == checklist + ) + + assert check_shortcut_signature( + Message.edit_checklist, + Bot.edit_message_checklist, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.edit_checklist, + message.get_bot(), + "edit_message_checklist", + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.edit_checklist, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_message_checklist", make_assertion) + assert await message.edit_checklist(checklist=checklist) + async def test_edit_media(self, monkeypatch, message): async def make_assertion(*_, **kwargs): chat_id = kwargs["chat_id"] == message.chat_id diff --git a/tests/test_ownedgift.py b/tests/test_ownedgift.py index b37794f3483..67ecfada2ee 100644 --- a/tests/test_ownedgift.py +++ b/tests/test_ownedgift.py @@ -18,7 +18,6 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import datetime as dtm -from collections.abc import Sequence from copy import deepcopy import pytest @@ -96,6 +95,7 @@ class OwnedGiftTestBase: prepaid_upgrade_star_count = 200 can_be_transferred = True transfer_star_count = 300 + next_transfer_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) class TestOwnedGiftWithoutRequest(OwnedGiftTestBase): @@ -139,6 +139,7 @@ def test_de_json_subclass(self, offline_bot, og_type, subclass, gift): "prepaid_upgrade_star_count": self.prepaid_upgrade_star_count, "can_be_transferred": self.can_be_transferred, "transfer_star_count": self.transfer_star_count, + "next_transfer_date": to_timestamp(self.next_transfer_date), } og = OwnedGift.de_json(json_dict, offline_bot) @@ -292,6 +293,7 @@ def owned_gift_unique(): is_saved=TestOwnedGiftUniqueWithoutRequest.is_saved, can_be_transferred=TestOwnedGiftUniqueWithoutRequest.can_be_transferred, transfer_star_count=TestOwnedGiftUniqueWithoutRequest.transfer_star_count, + next_transfer_date=TestOwnedGiftUniqueWithoutRequest.next_transfer_date, ) @@ -313,6 +315,7 @@ def test_de_json(self, offline_bot): "is_saved": self.is_saved, "can_be_transferred": self.can_be_transferred, "transfer_star_count": self.transfer_star_count, + "next_transfer_date": to_timestamp(self.next_transfer_date), } ogu = OwnedGiftUnique.de_json(json_dict, offline_bot) assert ogu.gift == self.unique_gift @@ -322,6 +325,7 @@ def test_de_json(self, offline_bot): assert ogu.is_saved == self.is_saved assert ogu.can_be_transferred == self.can_be_transferred assert ogu.transfer_star_count == self.transfer_star_count + assert ogu.next_transfer_date == self.next_transfer_date assert ogu.api_kwargs == {} def test_to_dict(self, owned_gift_unique): @@ -335,6 +339,7 @@ def test_to_dict(self, owned_gift_unique): assert json_dict["is_saved"] == self.is_saved assert json_dict["can_be_transferred"] == self.can_be_transferred assert json_dict["transfer_star_count"] == self.transfer_star_count + assert json_dict["next_transfer_date"] == to_timestamp(self.next_transfer_date) def test_equality(self, owned_gift_unique): a = owned_gift_unique @@ -365,7 +370,7 @@ def owned_gifts(request): class OwnedGiftsTestBase: total_count = 2 next_offset = "next_offset_str" - gifts: Sequence[OwnedGifts] = [ + gifts: list[OwnedGift] = [ OwnedGiftRegular( gift=Gift( id="id1", diff --git a/tests/test_reply.py b/tests/test_reply.py index ad95de4bfe6..6d29c910761 100644 --- a/tests/test_reply.py +++ b/tests/test_reply.py @@ -24,6 +24,8 @@ from telegram import ( BotCommand, Chat, + Checklist, + ChecklistTask, ExternalReplyInfo, Giveaway, LinkPreviewOptions, @@ -47,6 +49,7 @@ def external_reply_info(): link_preview_options=ExternalReplyInfoTestBase.link_preview_options, giveaway=ExternalReplyInfoTestBase.giveaway, paid_media=ExternalReplyInfoTestBase.paid_media, + checklist=ExternalReplyInfoTestBase.checklist, ) @@ -63,6 +66,13 @@ class ExternalReplyInfoTestBase: 1, ) paid_media = PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)]) + checklist = Checklist( + title="Checklist Title", + tasks=[ + ChecklistTask(text="Item 1", id=1), + ChecklistTask(text="Item 2", id=2), + ], + ) class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase): @@ -81,6 +91,7 @@ def test_de_json(self, offline_bot): "link_preview_options": self.link_preview_options.to_dict(), "giveaway": self.giveaway.to_dict(), "paid_media": self.paid_media.to_dict(), + "checklist": self.checklist.to_dict(), } external_reply_info = ExternalReplyInfo.de_json(json_dict, offline_bot) @@ -92,6 +103,7 @@ def test_de_json(self, offline_bot): assert external_reply_info.link_preview_options == self.link_preview_options assert external_reply_info.giveaway == self.giveaway assert external_reply_info.paid_media == self.paid_media + assert external_reply_info.checklist == self.checklist def test_to_dict(self, external_reply_info): ext_reply_info_dict = external_reply_info.to_dict() @@ -103,6 +115,7 @@ def test_to_dict(self, external_reply_info): assert ext_reply_info_dict["link_preview_options"] == self.link_preview_options.to_dict() assert ext_reply_info_dict["giveaway"] == self.giveaway.to_dict() assert ext_reply_info_dict["paid_media"] == self.paid_media.to_dict() + assert ext_reply_info_dict["checklist"] == self.checklist.to_dict() def test_equality(self, external_reply_info): a = external_reply_info diff --git a/tests/test_uniquegift.py b/tests/test_uniquegift.py index 051974b959b..926cdcae91f 100644 --- a/tests/test_uniquegift.py +++ b/tests/test_uniquegift.py @@ -17,6 +17,8 @@ # 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 as dtm + import pytest from telegram import ( @@ -29,6 +31,8 @@ UniqueGiftModel, UniqueGiftSymbol, ) +from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import UniqueGiftInfoOrigin from tests.auxil.slots import mro_slots @@ -383,6 +387,8 @@ def unique_gift_info(): origin=UniqueGiftInfoTestBase.origin, owned_gift_id=UniqueGiftInfoTestBase.owned_gift_id, transfer_star_count=UniqueGiftInfoTestBase.transfer_star_count, + last_resale_star_count=UniqueGiftInfoTestBase.last_resale_star_count, + next_transfer_date=UniqueGiftInfoTestBase.next_transfer_date, ) @@ -410,6 +416,8 @@ class UniqueGiftInfoTestBase: origin = UniqueGiftInfo.UPGRADE owned_gift_id = "some_id" transfer_star_count = 10 + last_resale_star_count = 5 + next_transfer_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) class TestUniqueGiftInfoWithoutRequest(UniqueGiftInfoTestBase): @@ -426,6 +434,8 @@ def test_de_json(self, offline_bot): "origin": self.origin, "owned_gift_id": self.owned_gift_id, "transfer_star_count": self.transfer_star_count, + "last_resale_star_count": self.last_resale_star_count, + "next_transfer_date": to_timestamp(self.next_transfer_date), } unique_gift_info = UniqueGiftInfo.de_json(json_dict, offline_bot) assert unique_gift_info.api_kwargs == {} @@ -433,6 +443,32 @@ def test_de_json(self, offline_bot): assert unique_gift_info.origin == self.origin assert unique_gift_info.owned_gift_id == self.owned_gift_id assert unique_gift_info.transfer_star_count == self.transfer_star_count + assert unique_gift_info.last_resale_star_count == self.last_resale_star_count + assert unique_gift_info.next_transfer_date == self.next_transfer_date + + def test_de_json_localization(self, tz_bot, offline_bot, raw_bot): + json_dict = { + "gift": self.gift.to_dict(), + "origin": self.origin, + "owned_gift_id": self.owned_gift_id, + "transfer_star_count": self.transfer_star_count, + "last_resale_star_count": self.last_resale_star_count, + "next_transfer_date": to_timestamp(self.next_transfer_date), + } + + unique_gift_info_raw = UniqueGiftInfo.de_json(json_dict, raw_bot) + unique_gift_info_offline = UniqueGiftInfo.de_json(json_dict, offline_bot) + unique_gift_info_tz = UniqueGiftInfo.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + unique_gift_info_tz_offset = unique_gift_info_tz.next_transfer_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + unique_gift_info_tz.next_transfer_date.replace(tzinfo=None) + ) + + assert unique_gift_info_raw.next_transfer_date.tzinfo == UTC + assert unique_gift_info_offline.next_transfer_date.tzinfo == UTC + assert unique_gift_info_tz_offset == tz_bot_offset def test_to_dict(self, unique_gift_info): json_dict = unique_gift_info.to_dict() @@ -440,6 +476,12 @@ def test_to_dict(self, unique_gift_info): assert json_dict["origin"] == self.origin assert json_dict["owned_gift_id"] == self.owned_gift_id assert json_dict["transfer_star_count"] == self.transfer_star_count + assert json_dict["last_resale_star_count"] == self.last_resale_star_count + assert json_dict["next_transfer_date"] == to_timestamp(self.next_transfer_date) + + def test_enum_type_conversion(self, unique_gift_info): + assert type(unique_gift_info.origin) is UniqueGiftInfoOrigin + assert unique_gift_info.origin == UniqueGiftInfoOrigin.UPGRADE def test_equality(self, unique_gift_info): a = unique_gift_info From 15ae1eac89c14ed4ecfbc8c25f2b1f3b8c38115f Mon Sep 17 00:00:00 2001 From: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:42:24 +0300 Subject: [PATCH 10/11] Documentation Improvements (#4839) Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- changes/unreleased/4839.gExNhF7C6kR5VrBg6cBaar.toml | 5 +++++ docs/auxil/sphinx_hooks.py | 4 +++- docs/source/telegram.constants.rst | 1 - src/telegram/_bot.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changes/unreleased/4839.gExNhF7C6kR5VrBg6cBaar.toml diff --git a/changes/unreleased/4839.gExNhF7C6kR5VrBg6cBaar.toml b/changes/unreleased/4839.gExNhF7C6kR5VrBg6cBaar.toml new file mode 100644 index 00000000000..30824ba789f --- /dev/null +++ b/changes/unreleased/4839.gExNhF7C6kR5VrBg6cBaar.toml @@ -0,0 +1,5 @@ +documentation = "Documentation Improvements. Among others, fix links to source code." +[[pull_requests]] +uid = "4839" +author_uid = "aelkheir" +closes_threads = ["4838"] diff --git a/docs/auxil/sphinx_hooks.py b/docs/auxil/sphinx_hooks.py index 47fd9c9281c..62d8613c8b0 100644 --- a/docs/auxil/sphinx_hooks.py +++ b/docs/auxil/sphinx_hooks.py @@ -55,6 +55,8 @@ } +# Resolves to the parent directory of `telegram/`, depending on installation setup, +# could either be `/src` or `/site-packages` FILE_ROOT = Path(inspect.getsourcefile(telegram)).parent.parent.resolve() @@ -161,7 +163,7 @@ def autodoc_process_docstring( with contextlib.suppress(Exception): source_lines, start_line = inspect.getsourcelines(obj) end_line = start_line + len(source_lines) - file = Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT) + file = Path("src") / Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT) LINE_NUMBERS[name] = (file, start_line, end_line) # Since we don't document the `__init__`, we call this manually to have it available for diff --git a/docs/source/telegram.constants.rst b/docs/source/telegram.constants.rst index ef1e6720107..618b35246f1 100644 --- a/docs/source/telegram.constants.rst +++ b/docs/source/telegram.constants.rst @@ -5,5 +5,4 @@ telegram.constants Module :members: :show-inheritance: :no-undoc-members: - :inherited-members: Enum, EnumMeta, str, int, float :exclude-members: __format__, __new__, __repr__, __str__ diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 1577cc38d16..a13ee1c2bb7 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -3177,7 +3177,7 @@ async def send_venue( google_place_id (:obj:`str`, optional): Google Places identifier of the venue. google_place_type (:obj:`str`, optional): Google Places type of the venue. (See `supported types \ - `_.) + `_.) disable_notification (:obj:`bool`, optional): |disable_notification| protect_content (:obj:`bool`, optional): |protect_content| From c062712472a1476b3b6b082dddd8f35977b5ecf7 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:01:41 +0200 Subject: [PATCH 11/11] Bump Version to v22.3 (#4870) --- .../4837.cVTsY7pxfQqgVXfFW9hgZK.toml | 0 .../4839.gExNhF7C6kR5VrBg6cBaar.toml | 0 .../4840.jz9uGugc5DUd8x8pQHPyzg.toml | 0 .../4841.HHVCCXXZbYAaaQVmKHCJm2.toml | 0 .../4842.PSW9ZbxENhwfRhbSfemCwE.toml | 0 .../4843.f5aZ5Vpxevw8fEEudawU5u.toml | 0 .../4847.8ujbbBbaZ2VTEdRLeqirSZ.toml | 0 .../4852.mz3RDjX636ZdGoR456C6v9.toml | 0 .../4855.8hCFRFMeMaRWpBEYaxrTMq.toml | 0 .../4858.ajt46xDsbfzFqcghJ2rP6g.toml | 0 .../4870.QeVo4BE5TNouLRJjCFbRMo.toml | 5 +++++ changes/config.py | 2 +- src/telegram/_bot.py | 6 +++--- src/telegram/_business.py | 2 +- src/telegram/_callbackquery.py | 2 +- src/telegram/_chat.py | 2 +- src/telegram/_chatfullinfo.py | 2 +- src/telegram/_checklists.py | 8 ++++---- src/telegram/_directmessagepricechanged.py | 2 +- src/telegram/_inputchecklist.py | 4 ++-- src/telegram/_message.py | 20 +++++++++---------- src/telegram/_ownedgift.py | 4 ++-- src/telegram/_reply.py | 4 ++-- src/telegram/_uniquegift.py | 14 ++++++------- src/telegram/_version.py | 2 +- src/telegram/constants.py | 18 ++++++++--------- src/telegram/ext/filters.py | 8 ++++---- 27 files changed, 55 insertions(+), 50 deletions(-) rename changes/{unreleased => 22.3_2025-07-20}/4837.cVTsY7pxfQqgVXfFW9hgZK.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4839.gExNhF7C6kR5VrBg6cBaar.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4840.jz9uGugc5DUd8x8pQHPyzg.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4841.HHVCCXXZbYAaaQVmKHCJm2.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4842.PSW9ZbxENhwfRhbSfemCwE.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4843.f5aZ5Vpxevw8fEEudawU5u.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4852.mz3RDjX636ZdGoR456C6v9.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4855.8hCFRFMeMaRWpBEYaxrTMq.toml (100%) rename changes/{unreleased => 22.3_2025-07-20}/4858.ajt46xDsbfzFqcghJ2rP6g.toml (100%) create mode 100644 changes/22.3_2025-07-20/4870.QeVo4BE5TNouLRJjCFbRMo.toml diff --git a/changes/unreleased/4837.cVTsY7pxfQqgVXfFW9hgZK.toml b/changes/22.3_2025-07-20/4837.cVTsY7pxfQqgVXfFW9hgZK.toml similarity index 100% rename from changes/unreleased/4837.cVTsY7pxfQqgVXfFW9hgZK.toml rename to changes/22.3_2025-07-20/4837.cVTsY7pxfQqgVXfFW9hgZK.toml diff --git a/changes/unreleased/4839.gExNhF7C6kR5VrBg6cBaar.toml b/changes/22.3_2025-07-20/4839.gExNhF7C6kR5VrBg6cBaar.toml similarity index 100% rename from changes/unreleased/4839.gExNhF7C6kR5VrBg6cBaar.toml rename to changes/22.3_2025-07-20/4839.gExNhF7C6kR5VrBg6cBaar.toml diff --git a/changes/unreleased/4840.jz9uGugc5DUd8x8pQHPyzg.toml b/changes/22.3_2025-07-20/4840.jz9uGugc5DUd8x8pQHPyzg.toml similarity index 100% rename from changes/unreleased/4840.jz9uGugc5DUd8x8pQHPyzg.toml rename to changes/22.3_2025-07-20/4840.jz9uGugc5DUd8x8pQHPyzg.toml diff --git a/changes/unreleased/4841.HHVCCXXZbYAaaQVmKHCJm2.toml b/changes/22.3_2025-07-20/4841.HHVCCXXZbYAaaQVmKHCJm2.toml similarity index 100% rename from changes/unreleased/4841.HHVCCXXZbYAaaQVmKHCJm2.toml rename to changes/22.3_2025-07-20/4841.HHVCCXXZbYAaaQVmKHCJm2.toml diff --git a/changes/unreleased/4842.PSW9ZbxENhwfRhbSfemCwE.toml b/changes/22.3_2025-07-20/4842.PSW9ZbxENhwfRhbSfemCwE.toml similarity index 100% rename from changes/unreleased/4842.PSW9ZbxENhwfRhbSfemCwE.toml rename to changes/22.3_2025-07-20/4842.PSW9ZbxENhwfRhbSfemCwE.toml diff --git a/changes/unreleased/4843.f5aZ5Vpxevw8fEEudawU5u.toml b/changes/22.3_2025-07-20/4843.f5aZ5Vpxevw8fEEudawU5u.toml similarity index 100% rename from changes/unreleased/4843.f5aZ5Vpxevw8fEEudawU5u.toml rename to changes/22.3_2025-07-20/4843.f5aZ5Vpxevw8fEEudawU5u.toml diff --git a/changes/unreleased/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml b/changes/22.3_2025-07-20/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml similarity index 100% rename from changes/unreleased/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml rename to changes/22.3_2025-07-20/4847.8ujbbBbaZ2VTEdRLeqirSZ.toml diff --git a/changes/unreleased/4852.mz3RDjX636ZdGoR456C6v9.toml b/changes/22.3_2025-07-20/4852.mz3RDjX636ZdGoR456C6v9.toml similarity index 100% rename from changes/unreleased/4852.mz3RDjX636ZdGoR456C6v9.toml rename to changes/22.3_2025-07-20/4852.mz3RDjX636ZdGoR456C6v9.toml diff --git a/changes/unreleased/4855.8hCFRFMeMaRWpBEYaxrTMq.toml b/changes/22.3_2025-07-20/4855.8hCFRFMeMaRWpBEYaxrTMq.toml similarity index 100% rename from changes/unreleased/4855.8hCFRFMeMaRWpBEYaxrTMq.toml rename to changes/22.3_2025-07-20/4855.8hCFRFMeMaRWpBEYaxrTMq.toml diff --git a/changes/unreleased/4858.ajt46xDsbfzFqcghJ2rP6g.toml b/changes/22.3_2025-07-20/4858.ajt46xDsbfzFqcghJ2rP6g.toml similarity index 100% rename from changes/unreleased/4858.ajt46xDsbfzFqcghJ2rP6g.toml rename to changes/22.3_2025-07-20/4858.ajt46xDsbfzFqcghJ2rP6g.toml diff --git a/changes/22.3_2025-07-20/4870.QeVo4BE5TNouLRJjCFbRMo.toml b/changes/22.3_2025-07-20/4870.QeVo4BE5TNouLRJjCFbRMo.toml new file mode 100644 index 00000000000..fe24e83ca2d --- /dev/null +++ b/changes/22.3_2025-07-20/4870.QeVo4BE5TNouLRJjCFbRMo.toml @@ -0,0 +1,5 @@ +other = "Bump Version to v22.3" +[[pull_requests]] +uid = "4870" +author_uid = "Bibo-Joshi" +closes_threads = [] diff --git a/changes/config.py b/changes/config.py index 1fd95fa9767..0cd01c2d575 100644 --- a/changes/config.py +++ b/changes/config.py @@ -80,7 +80,7 @@ def release(self, version: Version) -> bool: """replace "14.5" with version.uid except in the contrib guide then call super """ - root = Path(__file__).parent.parent / "telegram" + root = Path(__file__).parent.parent / "src" python_files = root.rglob("*.py") pattern = re.compile(r"NEXT\.VERSION") excluded_paths = {root / "docs/source/contribute.rst"} diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index a13ee1c2bb7..c5ea0a7b182 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -7578,7 +7578,7 @@ async def send_checklist( """ Use this method to send a checklist on behalf of a connected business account. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: business_connection_id (:obj:`str`): @@ -7653,7 +7653,7 @@ async def edit_message_checklist( """ Use this method to edit a checklist on behalf of a connected business account. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: business_connection_id (:obj:`str`): @@ -11220,7 +11220,7 @@ async def get_my_star_balance( ) -> StarAmount: """A method to get the current Telegram Stars balance of the bot. Requires no parameters. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Returns: :class:`telegram.StarAmount` diff --git a/src/telegram/_business.py b/src/telegram/_business.py index e9001d4bdf9..e2eaf4e64e4 100644 --- a/src/telegram/_business.py +++ b/src/telegram/_business.py @@ -191,7 +191,7 @@ class BusinessConnection(TelegramObject): .. versionchanged:: 22.1 Equality comparison now considers :attr:`rights` instead of ``can_reply``. - .. versionremoved:: NEXT.VERSION + .. versionremoved:: 22.3 Removed argument and attribute ``can_reply`` deprecated by API 9.0. Args: diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 49ae9159660..39c19e6f3f0 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -364,7 +364,7 @@ async def edit_message_checklist( For the documentation of the arguments, please see :meth:`telegram.Message.edit_checklist`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Returns: :class:`telegram.Message`: On success, the edited Message is returned. diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 4efb4fd80bb..9ebe007d5b9 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -1496,7 +1496,7 @@ async def send_checklist( For the documentation of the arguments, please see :meth:`telegram.Bot.send_checklist`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Returns: :class:`telegram.Message`: On success, instance representing the message posted. diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index 01439536b84..e6e1e5f0d9b 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -60,7 +60,7 @@ class ChatFullInfo(_ChatBase): object. Previously those were only available because this class inherited from :class:`telegram.Chat`. - .. versionremoved:: NEXT.VERSION + .. versionremoved:: 22.3 Removed argument and attribute ``can_send_gift`` deprecated by API 9.0. Args: diff --git a/src/telegram/_checklists.py b/src/telegram/_checklists.py index 818fd5981f5..3726f60f149 100644 --- a/src/telegram/_checklists.py +++ b/src/telegram/_checklists.py @@ -41,7 +41,7 @@ class ChecklistTask(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if all their :attr:`id` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: id (:obj:`int`): Unique identifier of the task. @@ -165,7 +165,7 @@ class Checklist(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if all their :attr:`tasks` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: title (:obj:`str`): Title of the checklist. @@ -276,7 +276,7 @@ class ChecklistTasksDone(TelegramObject): Two objects of this class are considered equal, if their :attr:`marked_as_done_task_ids` and :attr:`marked_as_not_done_task_ids` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: checklist_message (:class:`telegram.Message`, optional): Message containing the checklist @@ -344,7 +344,7 @@ class ChecklistTasksAdded(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`tasks` are equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: checklist_message (:class:`telegram.Message`, optional): Message containing the checklist diff --git a/src/telegram/_directmessagepricechanged.py b/src/telegram/_directmessagepricechanged.py index 9d4ed925137..ae50f06adec 100644 --- a/src/telegram/_directmessagepricechanged.py +++ b/src/telegram/_directmessagepricechanged.py @@ -30,7 +30,7 @@ class DirectMessagePriceChanged(TelegramObject): Describes a service message about a change in the price of direct messages sent to a channel chat. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`are_direct_messages_enabled`, and diff --git a/src/telegram/_inputchecklist.py b/src/telegram/_inputchecklist.py index 2608c99ccf7..5b8e8eea019 100644 --- a/src/telegram/_inputchecklist.py +++ b/src/telegram/_inputchecklist.py @@ -34,7 +34,7 @@ class InputChecklistTask(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`id` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: id (:obj:`int`): @@ -104,7 +104,7 @@ class InputChecklist(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`tasks` is equal. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Args: title (:obj:`str`): diff --git a/src/telegram/_message.py b/src/telegram/_message.py index d1a155ce4d7..16a4cd65ea3 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -529,7 +529,7 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: 20.0 checklist (:class:`telegram.Checklist`, optional): Message is a checklist - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 users_shared (:class:`telegram.UsersShared`, optional): Service message: users were shared with the bot @@ -610,11 +610,11 @@ class Message(MaybeInaccessibleMessage): checklist_tasks_done (:class:`telegram.ChecklistTasksDone`, optional): Service message: some tasks in a checklist were marked as done or not done - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 checklist_tasks_added (:class:`telegram.ChecklistTasksAdded`, optional): Service message: tasks were added to a checklist - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 paid_media (:class:`telegram.PaidMediaInfo`, optional): Message contains paid media; information about the paid media. @@ -627,7 +627,7 @@ class Message(MaybeInaccessibleMessage): Service message: the price for paid messages in the corresponding direct messages chat of a channel has changed. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. In specific instances @@ -889,7 +889,7 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: 20.0 checklist (:class:`telegram.Checklist`): Optional. Message is a checklist - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 users_shared (:class:`telegram.UsersShared`): Optional. Service message: users were shared with the bot @@ -971,11 +971,11 @@ class Message(MaybeInaccessibleMessage): checklist_tasks_done (:class:`telegram.ChecklistTasksDone`): Optional. Service message: some tasks in a checklist were marked as done or not done - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 checklist_tasks_added (:class:`telegram.ChecklistTasksAdded`): Optional. Service message: tasks were added to a checklist - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 paid_media (:class:`telegram.PaidMediaInfo`): Optional. Message contains paid media; information about the paid media. @@ -988,7 +988,7 @@ class Message(MaybeInaccessibleMessage): Optional. Service message: the price for paid messages in the corresponding direct messages chat of a channel has changed. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a @@ -3338,7 +3338,7 @@ async def reply_checklist( For the documentation of the arguments, please see :meth:`telegram.Bot.send_checklist`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Keyword Args: do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| @@ -3998,7 +3998,7 @@ async def edit_checklist( For the documentation of the arguments, please see :meth:`telegram.Bot.edit_message_checklist`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Note: You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family diff --git a/src/telegram/_ownedgift.py b/src/telegram/_ownedgift.py index 8efdccfe111..eca37272799 100644 --- a/src/telegram/_ownedgift.py +++ b/src/telegram/_ownedgift.py @@ -357,7 +357,7 @@ class OwnedGiftUnique(OwnedGift): next_transfer_date (:obj:`datetime.datetime`, optional): Date when the gift can be transferred. If it's in the past, then the gift can be transferred now. |datetime_localization| - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Attributes: type (:obj:`str`): Type of the owned gift, always :tg-const:`~telegram.OwnedGift.UNIQUE`. @@ -376,7 +376,7 @@ class OwnedGiftUnique(OwnedGift): next_transfer_date (:obj:`datetime.datetime`): Optional. Date when the gift can be transferred. If it's in the past, then the gift can be transferred now. |datetime_localization| - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ __slots__ = ( diff --git a/src/telegram/_reply.py b/src/telegram/_reply.py index 97c88215869..2887937afe2 100644 --- a/src/telegram/_reply.py +++ b/src/telegram/_reply.py @@ -92,7 +92,7 @@ class ExternalReplyInfo(TelegramObject): a spoiler animation. checklist (:class:`telegram.Checklist`, optional): Message is a checklist - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 contact (:class:`telegram.Contact`, optional): Message is a shared contact, information about the contact. dice (:class:`telegram.Dice`, optional): Message is a dice with random value. @@ -144,7 +144,7 @@ class ExternalReplyInfo(TelegramObject): a spoiler animation. checklist (:class:`telegram.Checklist`): Optional. Message is a checklist - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information about the contact. dice (:class:`telegram.Dice`): Optional. Message is a dice with random value. diff --git a/src/telegram/_uniquegift.py b/src/telegram/_uniquegift.py index 9c2efae7faa..31d47372fe8 100644 --- a/src/telegram/_uniquegift.py +++ b/src/telegram/_uniquegift.py @@ -346,7 +346,7 @@ class UniqueGiftInfo(TelegramObject): upgraded from regular gifts, :attr:`TRANSFER` for gifts transferred from other users or channels, or :attr:`RESALE` for gifts bought from other users. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.3 The :attr:`RESALE` origin was added. owned_gift_id (:obj:`str`, optional) Unique identifier of the received gift for the bot; only present for gifts received on behalf of business accounts. @@ -355,12 +355,12 @@ class UniqueGiftInfo(TelegramObject): last_resale_star_count (:obj:`int`, optional): For gifts bought from other users, the price paid for the gift. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 next_transfer_date (:obj:`datetime.datetime`, optional): Date when the gift can be transferred. If it's in the past, then the gift can be transferred now. |datetime_localization| - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 Attributes: gift (:class:`UniqueGift`): Information about the gift. @@ -368,7 +368,7 @@ class UniqueGiftInfo(TelegramObject): upgraded from regular gifts, :attr:`TRANSFER` for gifts transferred from other users or channels, or :attr:`RESALE` for gifts bought from other users. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.3 The :attr:`RESALE` origin was added. owned_gift_id (:obj:`str`) Optional. Unique identifier of the received gift for the bot; only present for gifts received on behalf of business accounts. @@ -377,12 +377,12 @@ class UniqueGiftInfo(TelegramObject): last_resale_star_count (:obj:`int`): Optional. For gifts bought from other users, the price paid for the gift. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 next_transfer_date (:obj:`datetime.datetime`): Optional. Date when the gift can be transferred. If it's in the past, then the gift can be transferred now. |datetime_localization| - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ UPGRADE: Final[str] = constants.UniqueGiftInfoOrigin.UPGRADE @@ -392,7 +392,7 @@ class UniqueGiftInfo(TelegramObject): RESALE: Final[str] = constants.UniqueGiftInfoOrigin.RESALE """:const:`telegram.constants.UniqueGiftInfoOrigin.RESALE` - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ __slots__ = ( diff --git a/src/telegram/_version.py b/src/telegram/_version.py index 64654ba8ed4..c2e38d26575 100644 --- a/src/telegram/_version.py +++ b/src/telegram/_version.py @@ -51,6 +51,6 @@ def __str__(self) -> str: __version_info__: Final[Version] = Version( - major=22, minor=2, micro=0, releaselevel="final", serial=0 + major=22, minor=3, micro=0, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) diff --git a/src/telegram/constants.py b/src/telegram/constants.py index 7cab2248713..a403a78e0cd 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -28,7 +28,7 @@ * Most of the constants in this module are grouped into enums. -.. versionremoved:: NEXT.VERSION +.. versionremoved:: 22.3 Removed deprecated class ``StarTransactions``. Please instead use :attr:`telegram.constants.Nanostar.VALUE`. """ @@ -1418,7 +1418,7 @@ class InputChecklistLimit(IntEnum): :class:`telegram.InputChecklistTask`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ __slots__ = () @@ -2104,17 +2104,17 @@ class MessageType(StringEnum): CHECKLIST = "checklist" """:obj:`str`: Messages with :attr:`telegram.Message.checklist`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ CHECKLIST_TASKS_ADDED = "checklist_tasks_added" """:obj:`str`: Messages with :attr:`telegram.Message.checklist_tasks_added`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ CHECKLIST_TASKS_DONE = "checklist_tasks_done" """:obj:`str`: Messages with :attr:`telegram.Message.checklist_tasks_done`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ CONNECTED_WEBSITE = "connected_website" """:obj:`str`: Messages with :attr:`telegram.Message.connected_website`.""" @@ -2127,7 +2127,7 @@ class MessageType(StringEnum): DIRECT_MESSAGE_PRICE_CHANGED = "direct_message_price_changed" """:obj:`str`: Messages with :attr:`telegram.Message.direct_message_price_changed`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ DOCUMENT = "document" """:obj:`str`: Messages with :attr:`telegram.Message.document`.""" @@ -2810,7 +2810,7 @@ class StarTransactionsLimit(IntEnum): .. versionadded:: 21.4 - .. versionremoved:: NEXT.VERSION + .. versionremoved:: 22.3 Removed deprecated attributes ``StarTransactionsLimit.NANOSTAR_MIN_AMOUNT`` and ``StarTransactionsLimit.NANOSTAR_MAX_AMOUNT``. Please instead use :attr:`telegram.constants.NanostarLimit.MIN_AMOUNT` @@ -3183,7 +3183,7 @@ class PollLimit(IntEnum): to the :paramref:`~telegram.Bot.send_poll.options` parameter of :meth:`telegram.Bot.send_poll`. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 22.3 This value was changed from ``10`` to ``12`` in accordance to Bot API 9.1. """ MAX_EXPLANATION_LENGTH = 200 @@ -3242,7 +3242,7 @@ class UniqueGiftInfoOrigin(StringEnum): RESALE = "resale" """:obj:`str` gift bought from other users - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index 6a5915063b4..94041114d73 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -931,7 +931,7 @@ def filter(self, message: Message) -> bool: CHECKLIST = _Checklist(name="filters.CHECKLIST") """Messages that contain :attr:`telegram.Message.checklist`. -.. versionadded:: NEXT.VERSION +.. versionadded:: 22.3 """ @@ -2015,7 +2015,7 @@ def filter(self, message: Message) -> bool: CHECKLIST_TASKS_ADDED = _ChecklistTasksAdded(name="filters.StatusUpdate.CHECKLIST_TASKS_ADDED") """Messages that contain :attr:`telegram.Message.checklist_tasks_added`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ class _ChecklistTasksDone(MessageFilter): @@ -2027,7 +2027,7 @@ def filter(self, message: Message) -> bool: CHECKLIST_TASKS_DONE = _ChecklistTasksDone(name="filters.StatusUpdate.CHECKLIST_TASKS_DONE") """Messages that contain :attr:`telegram.Message.checklist_tasks_done`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ class _ConnectedWebsite(MessageFilter): @@ -2050,7 +2050,7 @@ def filter(self, message: Message) -> bool: ) """Messages that contain :attr:`telegram.Message.direct_message_price_changed`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 22.3 """ class _DeleteChatPhoto(MessageFilter):