mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-19 15:45:13 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1fff6d90a | |||
| 31af1a9db8 | |||
| 4441543043 | |||
| 646ba37391 | |||
| 817b71d914 | |||
| 434cbfade8 | |||
| 34832d9db9 | |||
| 07225b9a02 | |||
| 0c06ba0a90 |
@@ -1,41 +0,0 @@
|
||||
name: Check Links in Documentation
|
||||
on:
|
||||
schedule:
|
||||
# First day of month at 05:46 in every 2nd month
|
||||
- cron: '46 5 1 */2 *'
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/docs-linkcheck.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test-sphinx-build:
|
||||
name: test-sphinx-linkcheck
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10']
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -r requirements-dev-all.txt
|
||||
- name: Check Links
|
||||
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck
|
||||
- name: Upload linkcheck output
|
||||
# Run also if the previous steps failed
|
||||
if: always()
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: linkcheck-output
|
||||
path: docs/build/html/output.*
|
||||
@@ -1,53 +0,0 @@
|
||||
name: Test Documentation Build
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- docs/**
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test-sphinx-build:
|
||||
name: test-sphinx-build
|
||||
runs-on: ${{matrix.os}}
|
||||
permissions:
|
||||
# for uploading artifacts
|
||||
actions: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10']
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements*.txt'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -r requirements-dev-all.txt
|
||||
- name: Test autogeneration of admonitions
|
||||
run: pytest -v --tb=short tests/docs/admonition_inserter.py
|
||||
- name: Build docs
|
||||
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto
|
||||
- name: Upload docs
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: HTML Docs
|
||||
retention-days: 7
|
||||
path: |
|
||||
# Exclude the .doctrees folder and .buildinfo file from the artifact
|
||||
# since they are not needed and add to the size
|
||||
docs/build/html/*
|
||||
!docs/build/html/.doctrees
|
||||
!docs/build/html/.buildinfo
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Bot API Tests
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- tests/**
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
# Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions
|
||||
- cron: '7 3 * * 1,5'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-conformity:
|
||||
name: check-conformity
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.11]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install .[all]
|
||||
python -W ignore -m pip install -r requirements-unit-tests.txt
|
||||
- name: Compare to official api
|
||||
run: |
|
||||
pytest -v tests/test_official/test_official.py --junit-xml=.test_report_official.xml
|
||||
exit $?
|
||||
env:
|
||||
TEST_OFFICIAL: "true"
|
||||
shell: bash --noprofile --norc {0}
|
||||
|
||||
- name: Test Summary
|
||||
id: test_summary
|
||||
uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
|
||||
if: always() # always run, even if tests fail
|
||||
with:
|
||||
paths: .test_report_official.xml
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Check Type Completeness
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- pyproject.toml
|
||||
- .github/workflows/type_completeness.yml
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test-type-completeness:
|
||||
name: test-type-completeness
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
|
||||
with:
|
||||
package-name: telegram
|
||||
python-version: 3.12
|
||||
pyright-version: ~=1.1.367
|
||||
File diff suppressed because one or more lines are too long
-48
@@ -4,54 +4,6 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 21.11
|
||||
=============
|
||||
|
||||
*Released 2025-03-01*
|
||||
|
||||
This is the technical changelog for version 21.11. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
|
||||
|
||||
Major Changes and New Features
|
||||
------------------------------
|
||||
|
||||
- Full Support for Bot API 8.3 (:pr:`4676` closes :issue:`4677`, :pr:`4682` by `aelkheir <https://github.com/aelkheir>`_, :pr:`4690` by `aelkheir <https://github.com/aelkheir>`_, :pr:`4691` by `aelkheir <https://github.com/aelkheir>`_)
|
||||
- Make ``provider_token`` Argument Optional (:pr:`4689`)
|
||||
- Remove Deprecated ``InlineQueryResultArticle.hide_url`` (:pr:`4640` closes :issue:`4638`)
|
||||
- Accept ``datetime.timedelta`` Input in ``Bot`` Method Parameters (:pr:`4651`)
|
||||
- Extend Customization Support for ``Bot.base_(file_)url`` (:pr:`4632` closes :issue:`3355`)
|
||||
- Support ``allow_paid_broadcast`` in ``AIORateLimiter`` (:pr:`4627` closes :issue:`4578`)
|
||||
- Add ``BaseUpdateProcessor.current_concurrent_updates`` (:pr:`4626` closes :issue:`3984`)
|
||||
|
||||
Minor Changes and Bug Fixes
|
||||
---------------------------
|
||||
|
||||
- Add Bootstrapping Logic to ``Application.run_*`` (:pr:`4673` closes :issue:`4657`)
|
||||
- Fix a Bug in ``edit_user_star_subscription`` (:pr:`4681` by `vavasik800 <https://github.com/vavasik800>`_)
|
||||
- Simplify Handling of Empty Data in ``TelegramObject.de_json`` and Friends (:pr:`4617` closes :issue:`4614`)
|
||||
|
||||
Documentation Improvements
|
||||
--------------------------
|
||||
|
||||
- Documentation Improvements (:pr:`4641`)
|
||||
- Overhaul Admonition Insertion in Documentation (:pr:`4462` closes :issue:`4414`)
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Stabilize Linkcheck Test (:pr:`4693`)
|
||||
- Bump ``pre-commit`` Hooks to Latest Versions (:pr:`4643`)
|
||||
- Refactor Tests for ``TelegramObject`` Classes with Subclasses (:pr:`4654` closes :issue:`4652`)
|
||||
- Use Fine Grained Permissions for GitHub Actions Workflows (:pr:`4668`)
|
||||
|
||||
Dependency Updates
|
||||
------------------
|
||||
|
||||
- Bump ``actions/setup-python`` from 5.3.0 to 5.4.0 (:pr:`4665`)
|
||||
- Bump ``dependabot/fetch-metadata`` from 2.2.0 to 2.3.0 (:pr:`4666`)
|
||||
- Bump ``actions/stale`` from 9.0.0 to 9.1.0 (:pr:`4667`)
|
||||
- Bump ``astral-sh/setup-uv`` from 5.1.0 to 5.2.2 (:pr:`4664`)
|
||||
- Bump ``codecov/test-results-action`` from 1.0.1 to 1.0.2 (:pr:`4663`)
|
||||
|
||||
Version 21.10
|
||||
=============
|
||||
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
:target: https://pypi.org/project/python-telegram-bot/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-8.3-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-8.2-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 **8.3** are natively supported by this library.
|
||||
All types and methods of the Telegram Bot API **8.2** are natively supported by this library.
|
||||
In addition, Bot API functionality not yet natively included can still be used as described `in our wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Forward-Compatibility>`_.
|
||||
|
||||
Notable Features
|
||||
|
||||
@@ -111,11 +111,6 @@ linkcheck_ignore = [
|
||||
# Anchors are apparently inserted by GitHub dynamically, so let's skip checking them
|
||||
"https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples#",
|
||||
r"https://github\.com/python-telegram-bot/python-telegram-bot/wiki/[\w\-_,]+\#",
|
||||
# The LGPL license link regularly causes network errors for some reason
|
||||
re.escape("https://www.gnu.org/licenses/lgpl-3.0.html"),
|
||||
# The doc-fixes branch may not always exist - doesn't matter, we only link to it from the
|
||||
# contributing guide
|
||||
re.escape("https://docs.python-telegram-bot.org/en/doc-fixes"),
|
||||
]
|
||||
linkcheck_allowed_redirects = {
|
||||
# Redirects to the default version are okay
|
||||
|
||||
@@ -27,7 +27,6 @@ Your bot can accept payments from Telegram users. Please see the `introduction t
|
||||
telegram.successfulpayment
|
||||
telegram.transactionpartner
|
||||
telegram.transactionpartneraffiliateprogram
|
||||
telegram.transactionpartnerchat
|
||||
telegram.transactionpartnerfragment
|
||||
telegram.transactionpartnerother
|
||||
telegram.transactionpartnertelegramads
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
TransactionPartnerChat
|
||||
======================
|
||||
|
||||
.. autoclass:: telegram.TransactionPartnerChat
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members: TransactionPartner
|
||||
@@ -98,6 +98,4 @@
|
||||
|
||||
.. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
.. |org-verify| replace:: `on behalf of the organization <https://telegram.org/verify#third-party-verification>`__
|
||||
|
||||
.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
|
||||
.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable=unused-argument
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
"""Simple state machine to handle user support.
|
||||
One admin is supported. The admin can have one active conversation at a time. Other users
|
||||
are put on hold until the admin finishes the current conversation.
|
||||
In each conversation, the admin and the user take turns to send messages.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
ContextTypes,
|
||||
FiniteStateMachine,
|
||||
MessageHandler,
|
||||
State,
|
||||
StateInfo,
|
||||
filters,
|
||||
)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserSupportMachine(FiniteStateMachine[Optional[int]]):
|
||||
|
||||
HOLD = State("HOLD")
|
||||
WELCOMING = State("WELCOMING")
|
||||
WAITING_FOR_REPLY = State("WAITING_FOR_REPLY")
|
||||
WRITING = State("WRITING")
|
||||
|
||||
def __init__(self, admin_id: int):
|
||||
self.admin_id = admin_id
|
||||
super().__init__()
|
||||
|
||||
def _get_admin_state(self) -> tuple[State, int]:
|
||||
return self._states[self.admin_id]
|
||||
|
||||
def get_state_info(self, update: object) -> StateInfo[Optional[int]]:
|
||||
if not isinstance(update, Update) or not (user := update.effective_user):
|
||||
key = None
|
||||
state, version = self.states[key]
|
||||
return StateInfo(key=key, state=state, version=version)
|
||||
|
||||
# Admin is easy - just return the state
|
||||
admin_state, admin_version = self._get_admin_state()
|
||||
if user.id == self.admin_id:
|
||||
logging.debug("Returning admin state: %s", admin_state)
|
||||
return StateInfo(self.admin_id, admin_state, admin_version)
|
||||
|
||||
# If the user state is active in the conversation, we can just return that state
|
||||
user_state, user_version = self._states[user.id]
|
||||
if user_state.matches(self.WELCOMING | self.WRITING | self.WAITING_FOR_REPLY):
|
||||
logging.debug("Returning user state: %s", user_state)
|
||||
return StateInfo(user.id, user_state, user_version)
|
||||
|
||||
# On first interaction, we need to determine what to do with the user
|
||||
# if the admin is not idle, we put the user on hold. Otherwise, they may send the first
|
||||
# message, and we put the admin in waiting for reply to avoid another user occupying the
|
||||
# admin first
|
||||
effective_user_state = self.HOLD if admin_state != State.IDLE else self.WELCOMING
|
||||
self._do_set_state(user.id, effective_user_state, user_version)
|
||||
if effective_user_state == self.WELCOMING:
|
||||
self._do_set_state(self.admin_id, self.WAITING_FOR_REPLY)
|
||||
|
||||
logging.debug("Returning user state: %s", effective_user_state)
|
||||
return StateInfo(user.id, effective_user_state, user_version)
|
||||
|
||||
|
||||
async def welcome_user(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.forward(context.bot_data["admin_id"])
|
||||
suffix = ""
|
||||
if UserSupportMachine.HOLD in context.fsm.get_state_history(context.fsm_state_info.key)[:-1]:
|
||||
suffix = " Thank you for patiently waiting. We hope you enjoyed the music."
|
||||
|
||||
await update.effective_message.reply_text(
|
||||
"Welcome! Your message has been forwarded to the admin. "
|
||||
f"They will get back to you soon.{suffix}"
|
||||
)
|
||||
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
|
||||
await context.fsm.set_state(context.bot_data["admin_id"], UserSupportMachine.WRITING)
|
||||
context.bot_data["active_user"] = update.effective_user.id
|
||||
|
||||
|
||||
async def conversation_timeout(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
active_user = context.bot_data.get("active_user")
|
||||
admin_id = context.bot_data["admin_id"]
|
||||
|
||||
async def handle(user_id: int) -> None:
|
||||
await context.bot.send_message(
|
||||
user_id, "The conversation has been stopped due to inactivity."
|
||||
)
|
||||
await context.fsm.set_state(user_id, State.IDLE)
|
||||
|
||||
if active_user:
|
||||
await handle(active_user)
|
||||
await handle(admin_id)
|
||||
|
||||
|
||||
async def handle_reply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not (active_user := context.bot_data.get("active_user")):
|
||||
logger.warning("No active user found, ignoring message")
|
||||
|
||||
target = (
|
||||
active_user
|
||||
if update.effective_user.id == (admin_id := context.bot_data["admin_id"])
|
||||
else admin_id
|
||||
)
|
||||
await context.bot.send_message(target, update.effective_message.text)
|
||||
logging.debug("Forwarded message to %s", target)
|
||||
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
|
||||
logging.debug("Done setting state to WAITING_FOR_REPLY for %s", target)
|
||||
await context.fsm.set_state(target, UserSupportMachine.WRITING)
|
||||
logging.debug("Done setting state to WRITING for %s, context.fsm_key")
|
||||
|
||||
context.fsm.schedule_timeout(
|
||||
when=30,
|
||||
callback=conversation_timeout,
|
||||
cancel_keys=[active_user, admin_id],
|
||||
)
|
||||
|
||||
|
||||
async def stop_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
text = "The conversation has been stopped."
|
||||
admin_id = context.bot_data["admin_id"]
|
||||
active_user = context.bot_data.get("active_user")
|
||||
|
||||
await context.bot.send_message(admin_id, text)
|
||||
await context.fsm.set_state(admin_id, State.IDLE)
|
||||
if active_user:
|
||||
await context.bot.send_message(active_user, text)
|
||||
await context.fsm.set_state(active_user, State.IDLE)
|
||||
|
||||
|
||||
async def hold_melody(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text(
|
||||
"You have been put on hold. The admin will get back to you soon. Please hear some music "
|
||||
"while you wait: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
)
|
||||
|
||||
|
||||
async def not_your_turn(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text(
|
||||
"It's not your turn yet. Please wait for the other party to reply to your message."
|
||||
)
|
||||
|
||||
|
||||
async def unsupported_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text("This message is not supported.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
application.fsm = UserSupportMachine(admin_id=123456)
|
||||
application.fsm.set_job_queue(application.job_queue)
|
||||
application.bot_data["admin_id"] = application.fsm.admin_id
|
||||
|
||||
# Users are welcomed only if they are in the corresponding state
|
||||
application.add_handler(
|
||||
MessageHandler(~filters.User(application.fsm.admin_id) & filters.TEXT, welcome_user),
|
||||
state=UserSupportMachine.WELCOMING,
|
||||
)
|
||||
|
||||
# Conversation logic:
|
||||
# * forward messages between user and admin
|
||||
# * stop the conversation at any time (admin or user)
|
||||
# * point out that the other party is currently writing
|
||||
# Important: Order matters!
|
||||
application.add_handler(
|
||||
CommandHandler("stop", stop_conversation),
|
||||
state=UserSupportMachine.WAITING_FOR_REPLY | UserSupportMachine.WRITING,
|
||||
)
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, handle_reply), state=UserSupportMachine.WRITING
|
||||
)
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, not_your_turn), state=UserSupportMachine.WAITING_FOR_REPLY
|
||||
)
|
||||
|
||||
# If the admin is busy, put the user on hold
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, hold_melody), state=UserSupportMachine.HOLD
|
||||
)
|
||||
|
||||
# Fallback
|
||||
application.add_handler(MessageHandler(filters.ALL, unsupported_message), state=State.ANY)
|
||||
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable=unused-argument
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
"""State machine bot showcasing how concurrency can be handled with FSM.
|
||||
How to use:
|
||||
|
||||
* Use Case 1: Concurrent balance updates
|
||||
- /unsafe_update <balance_update>: Unsafe update of the wallet balance. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect
|
||||
- /safe_update <balance_update>: Safe update of the wallet balance. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect
|
||||
|
||||
* Use Case 2: Declare a winner - who is the fastest?
|
||||
- /unsafe_declare_winner: Unsafe declaration of the user as winner. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
|
||||
after the winner is declared.
|
||||
- /safe_declare_winner: Safe declaration of the user as winner. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
|
||||
after the winner is declared.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
ContextTypes,
|
||||
FiniteStateMachine,
|
||||
MessageHandler,
|
||||
State,
|
||||
StateInfo,
|
||||
filters,
|
||||
)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConcurrentMachine(FiniteStateMachine[None]):
|
||||
"""This FSM only knows a global state for the whole bot"""
|
||||
|
||||
UPDATING_BALANCE = State("UPDATING_BALANCE")
|
||||
WINNER_DECLARED = State("WINNER_DECLARED")
|
||||
|
||||
def get_state_info(self, update: object) -> StateInfo[None]:
|
||||
state, version = self.states[None]
|
||||
return StateInfo(key=None, state=state, version=version)
|
||||
|
||||
|
||||
########################################
|
||||
# Use case 1: Concurrent balance updates
|
||||
########################################
|
||||
|
||||
|
||||
async def update_balance(context: ContextTypes.DEFAULT_TYPE, update: Update) -> None:
|
||||
initial_balance = context.bot_data.get("balance", 0)
|
||||
balance_update = int(context.args[0])
|
||||
# Simulate heavy computation
|
||||
await update.effective_message.reply_text(
|
||||
f"Initiating balance update: {initial_balance}. Updating ..."
|
||||
)
|
||||
await update.effective_chat.send_action(ChatAction.TYPING)
|
||||
await asyncio.sleep(4.5)
|
||||
new_balance = context.bot_data["balance"] = initial_balance + balance_update
|
||||
await update.effective_message.reply_text(f"Balance updated. New balance: {new_balance}")
|
||||
|
||||
|
||||
async def unsafe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Unsafe update of the wallet balance"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.UPDATING_BALANCE)
|
||||
|
||||
# At this point, the lock is released such that multiple updates can update
|
||||
# the balance concurrently. This can lead to race conditions.
|
||||
await update_balance(context, update)
|
||||
|
||||
await context.fsm.set_state(context.fsm_state_info.key, State.IDLE)
|
||||
|
||||
|
||||
async def safe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Safe update of the wallet balance"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async with context.as_fsm_state(ConcurrentMachine.UPDATING_BALANCE):
|
||||
# At this point, the lock is acquired such that only one update can update
|
||||
# the balance at a time. This prevents race conditions.
|
||||
await update_balance(context, update)
|
||||
|
||||
|
||||
async def busy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Busy state"""
|
||||
await update.effective_message.reply_text("I'm busy, try again later.")
|
||||
|
||||
|
||||
####################################################
|
||||
# Use case 2: Declare a winner - who is the fastest?
|
||||
####################################################
|
||||
|
||||
|
||||
async def declare_winner_unsafe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Declare the user as winner"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Unsafe state update: No version check, so the state might have already changed
|
||||
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.WINNER_DECLARED)
|
||||
await update.effective_message.reply_text("You are the winner!")
|
||||
|
||||
|
||||
async def declare_winner_safe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Declare the user as winner"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
await context.set_state(ConcurrentMachine.WINNER_DECLARED)
|
||||
await update.effective_message.reply_text("You are the winner!")
|
||||
except ValueError:
|
||||
await update.effective_message.reply_text(
|
||||
"Sorry, you are too late. Someone else was faster."
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = Application.builder().token("TOKEN").concurrent_updates(True).build()
|
||||
application.fsm = ConcurrentMachine()
|
||||
|
||||
# Note: OR-combination of states is used here to allow both use cases to be handled
|
||||
# in parallel. Not really necessary for the showcasing, just a nice touch :)
|
||||
|
||||
# Use case 2: Declare a winner - who is the fastest?
|
||||
application.add_handler(
|
||||
CommandHandler("unsafe_declare_winner", declare_winner_unsafe),
|
||||
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
|
||||
)
|
||||
application.add_handler(
|
||||
CommandHandler("safe_declare_winner", declare_winner_safe),
|
||||
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
|
||||
)
|
||||
|
||||
# Use case 1: Concurrent balance updates
|
||||
application.add_handler(
|
||||
CommandHandler("unsafe_update", unsafe_update, has_args=1),
|
||||
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
|
||||
)
|
||||
application.add_handler(
|
||||
CommandHandler("safe_update", safe_update, has_args=1),
|
||||
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
|
||||
)
|
||||
# Order matters, so this needs to be added last
|
||||
application.add_handler(
|
||||
MessageHandler(filters.ALL, busy), state=ConcurrentMachine.UPDATING_BALANCE
|
||||
)
|
||||
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -61,9 +61,9 @@ async def start_with_shipping_callback(update: Update, context: ContextTypes.DEF
|
||||
title,
|
||||
description,
|
||||
payload,
|
||||
PAYMENT_PROVIDER_TOKEN,
|
||||
currency,
|
||||
prices,
|
||||
provider_token=PAYMENT_PROVIDER_TOKEN,
|
||||
need_name=True,
|
||||
need_phone_number=True,
|
||||
need_email=True,
|
||||
@@ -90,13 +90,7 @@ async def start_without_shipping_callback(
|
||||
# optionally pass need_name=True, need_phone_number=True,
|
||||
# need_email=True, need_shipping_address=True, is_flexible=True
|
||||
await context.bot.send_invoice(
|
||||
chat_id,
|
||||
title,
|
||||
description,
|
||||
payload,
|
||||
currency,
|
||||
prices,
|
||||
provider_token=PAYMENT_PROVIDER_TOKEN,
|
||||
chat_id, title, description, payload, PAYMENT_PROVIDER_TOKEN, currency, prices
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -238,7 +238,6 @@ __all__ = (
|
||||
"TextQuote",
|
||||
"TransactionPartner",
|
||||
"TransactionPartnerAffiliateProgram",
|
||||
"TransactionPartnerChat",
|
||||
"TransactionPartnerFragment",
|
||||
"TransactionPartnerOther",
|
||||
"TransactionPartnerTelegramAds",
|
||||
@@ -276,7 +275,6 @@ from telegram._payment.stars.startransactions import StarTransaction, StarTransa
|
||||
from telegram._payment.stars.transactionpartner import (
|
||||
TransactionPartner,
|
||||
TransactionPartnerAffiliateProgram,
|
||||
TransactionPartnerChat,
|
||||
TransactionPartnerFragment,
|
||||
TransactionPartnerOther,
|
||||
TransactionPartnerTelegramAds,
|
||||
|
||||
+41
-83
@@ -245,7 +245,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
Example:
|
||||
``"https://api.telegram.org/bot{token}/test"``
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
base_file_url (:obj:`str`, optional): Telegram Bot API file URL.
|
||||
If the string contains ``{token}``, it will be replaced with the bot's
|
||||
@@ -262,7 +262,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
Example:
|
||||
``"https://api.telegram.org/file/bot{token}/test"``
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
request (:class:`telegram.request.BaseRequest`, optional): Pre initialized
|
||||
:class:`telegram.request.BaseRequest` instances. Will be used for all bot methods
|
||||
@@ -1218,7 +1218,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1243,10 +1242,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
original message was sent (or channel username in the format ``@channelusername``).
|
||||
message_id (:obj:`int`): Message identifier in the chat specified in
|
||||
:paramref:`from_chat_id`.
|
||||
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
|
||||
forwarded video in the message
|
||||
|
||||
.. versionadded:: 21.11
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
protect_content (:obj:`bool`, optional): |protect_content|
|
||||
|
||||
@@ -1265,7 +1260,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"chat_id": chat_id,
|
||||
"from_chat_id": from_chat_id,
|
||||
"message_id": message_id,
|
||||
"video_start_timestamp": video_start_timestamp,
|
||||
}
|
||||
|
||||
return await self._send_message(
|
||||
@@ -1564,7 +1558,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent audio
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
performer (:obj:`str`, optional): Performer.
|
||||
title (:obj:`str`, optional): Track name.
|
||||
@@ -1961,8 +1955,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
@@ -2006,17 +1998,10 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
width (:obj:`int`, optional): Video width.
|
||||
height (:obj:`int`, optional): Video height.
|
||||
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): Cover for the video in the message. |fileinputnopath|
|
||||
|
||||
.. versionadded:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
caption (:obj:`str`, optional): Video caption (may also be used when resending videos
|
||||
by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH`
|
||||
characters after entities parsing.
|
||||
@@ -2103,8 +2088,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"width": width,
|
||||
"height": height,
|
||||
"supports_streaming": supports_streaming,
|
||||
"cover": self._parse_file_input(cover, attach=True) if cover else None,
|
||||
"start_timestamp": start_timestamp,
|
||||
"thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None,
|
||||
"has_spoiler": has_spoiler,
|
||||
"show_caption_above_media": show_caption_above_media,
|
||||
@@ -2192,7 +2175,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
|
||||
message.
|
||||
@@ -2344,7 +2327,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent
|
||||
animation in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
width (:obj:`int`, optional): Animation width.
|
||||
height (:obj:`int`, optional): Animation height.
|
||||
@@ -2528,7 +2511,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the voice
|
||||
message in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
protect_content (:obj:`bool`, optional): |protect_content|
|
||||
@@ -2842,7 +2825,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
|
||||
locations that can be edited indefinitely.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
heading (:obj:`int`, optional): For live locations, a direction in which the user is
|
||||
moving, in degrees. Must be between
|
||||
@@ -3012,7 +2995,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
|
||||
.. versionadded:: 21.2.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
business_connection_id (:obj:`str`, optional): |business_id_str_edit|
|
||||
|
||||
@@ -3709,7 +3692,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
time in seconds that the
|
||||
result of the inline query may be cached on the server. Defaults to ``300``.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
is_personal (:obj:`bool`, optional): Pass :obj:`True`, if results may be cached on
|
||||
the server side only for the user that sent the query. By default,
|
||||
@@ -4161,7 +4144,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
time in seconds that the
|
||||
result of the callback query may be cached client-side. Defaults to 0.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
|
||||
Returns:
|
||||
@@ -4648,11 +4631,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"""
|
||||
Use this method to specify a url and receive incoming updates via an outgoing webhook.
|
||||
Whenever there is an update for the bot, Telegram will send an HTTPS POST request to the
|
||||
specified url, containing An Update. In case of an unsuccessful request
|
||||
(a request with response
|
||||
`HTTP status code <https://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_different
|
||||
from ``2XY``),
|
||||
Telegram will repeat the request and give up after a reasonable amount of attempts.
|
||||
specified url, containing An Update. In case of an unsuccessful request,
|
||||
Telegram will give up after a reasonable amount of attempts.
|
||||
|
||||
If you'd like to make sure that the Webhook was set by you, you can specify secret data in
|
||||
the parameter :paramref:`secret_token`. If specified, the request will contain a header
|
||||
@@ -5197,9 +5177,9 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str], # This arg is now optional as of Bot API 7.4
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -5252,13 +5232,13 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
|
||||
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
|
||||
displayed to the user, use it for your internal processes.
|
||||
provider_token (:obj:`str`, optional): Payments provider token, obtained via
|
||||
provider_token (:obj:`str`): Payments provider token, obtained via
|
||||
`@BotFather <https://t.me/BotFather>`_. Pass an empty string for payments in
|
||||
|tg_stars|.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Bot API 7.4 made this parameter is optional and this is now reflected in the
|
||||
function signature.
|
||||
.. deprecated:: 21.3
|
||||
As of Bot API 7.4, this parameter is now optional and future versions of the
|
||||
library will make it optional as well.
|
||||
|
||||
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
|
||||
<https://core.telegram.org/bots/payments#supported-currencies>`_. Pass ``XTR`` for
|
||||
@@ -6984,7 +6964,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
|
||||
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
|
||||
``.WEBM`` video.
|
||||
WEBM video.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
@@ -6998,7 +6978,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE`
|
||||
kilobytes in size; see
|
||||
`the docs <https://core.telegram.org/stickers#animation-requirements>`_ for
|
||||
animated sticker technical requirements, or a ``.WEBM`` video with the thumbnail up
|
||||
animated sticker technical requirements, or a **.WEBM** video with the thumbnail up
|
||||
to :tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE`
|
||||
kilobytes in size; see
|
||||
`this <https://core.telegram.org/stickers#video-requirements>`_ for video sticker
|
||||
@@ -7387,7 +7367,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.Poll.MAX_OPEN_PERIOD`. Can't be used together with
|
||||
:paramref:`close_date`.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix
|
||||
timestamp) when the poll will be automatically closed. Must be at least
|
||||
@@ -7994,7 +7974,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
@@ -8014,10 +7993,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the
|
||||
original message was sent (or channel username in the format ``@channelusername``).
|
||||
message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id.
|
||||
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
|
||||
copied video in the message
|
||||
|
||||
.. versionadded:: 21.11
|
||||
caption (:obj:`str`, optional): New caption for media,
|
||||
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
|
||||
entities parsing. If not specified, the original caption is kept.
|
||||
@@ -8108,7 +8083,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
"reply_parameters": reply_parameters,
|
||||
"show_caption_above_media": show_caption_above_media,
|
||||
"allow_paid_broadcast": allow_paid_broadcast,
|
||||
"video_start_timestamp": video_start_timestamp,
|
||||
}
|
||||
|
||||
result = await self._post(
|
||||
@@ -8278,9 +8252,9 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str], # This arg is now optional as of Bot API 7.4
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[Sequence[int]] = None,
|
||||
provider_data: Optional[Union[str, object]] = None,
|
||||
@@ -8322,13 +8296,13 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
|
||||
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
|
||||
displayed to the user, use it for your internal processes.
|
||||
provider_token (:obj:`str`, optional): Payments provider token, obtained via
|
||||
provider_token (:obj:`str`): Payments provider token, obtained via
|
||||
`@BotFather <https://t.me/BotFather>`_. Pass an empty string for payments in
|
||||
|tg_stars|.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Bot API 7.4 made this parameter is optional and this is now reflected in the
|
||||
function signature.
|
||||
.. deprecated:: 21.3
|
||||
As of Bot API 7.4, this parameter is now optional and future versions of the
|
||||
library will make it optional as well.
|
||||
|
||||
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
|
||||
<https://core.telegram.org/bots/payments#supported-currencies>`_. Pass ``XTR`` for
|
||||
@@ -9300,8 +9274,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Use this method to change the chosen reactions on a message. Service messages of some types
|
||||
can't be
|
||||
Use this method to change the chosen reactions on a message. Service messages can't be
|
||||
reacted to. Automatically forwarded messages from a channel to its discussion group have
|
||||
the same available reactions as messages in the channel. Bots can't use paid reactions.
|
||||
|
||||
@@ -9588,7 +9561,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
"is_canceled": is_canceled,
|
||||
}
|
||||
return await self._post(
|
||||
"editUserStarSubscription",
|
||||
"editUserStartSubscription",
|
||||
data,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -9734,7 +9707,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
active for before the next payment. Currently, it must always be
|
||||
:tg-const:`telegram.constants.ChatSubscriptionLimit.SUBSCRIPTION_PERIOD` (30 days).
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
subscription_price (:obj:`int`): The number of Telegram Stars a user must pay initially
|
||||
and after each subsequent subscription period to be a member of the chat;
|
||||
@@ -9831,7 +9804,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> Gifts:
|
||||
"""Returns the list of gifts that can be sent by the bot to users and channel chats.
|
||||
"""Returns the list of gifts that can be sent by the bot to users.
|
||||
Requires no parameters.
|
||||
|
||||
.. versionadded:: 21.8
|
||||
@@ -9855,13 +9828,12 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
async def send_gift(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
gift_id: Union[str, Gift] = None, # type: ignore
|
||||
user_id: int,
|
||||
gift_id: Union[str, Gift],
|
||||
text: Optional[str] = None,
|
||||
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
text_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
pay_for_upgrade: Optional[bool] = None,
|
||||
chat_id: Optional[Union[str, int]] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -9869,23 +9841,15 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Sends a gift to the given user or channel chat.
|
||||
The gift can't be converted to Telegram Stars by the receiver.
|
||||
"""Sends a gift to the given user.
|
||||
The gift can't be converted to Telegram Stars by the user
|
||||
|
||||
.. versionadded:: 21.8
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`, optional): Required if :paramref:`chat_id` is not specified.
|
||||
Unique identifier of the target user that will receive the gift.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Now optional.
|
||||
user_id (:obj:`int`): Unique identifier of the target user that will receive the gift
|
||||
gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a
|
||||
:class:`~telegram.Gift` object
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`user_id`
|
||||
is not specified. |chat_id_channel| It will receive the gift.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
text (:obj:`str`, optional): Text that will be shown along with the gift;
|
||||
0- :tg-const:`telegram.constants.GiftLimit.MAX_TEXT_LENGTH` characters
|
||||
text_parse_mode (:obj:`str`, optional): Mode for parsing entities.
|
||||
@@ -9912,11 +9876,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
Raises:
|
||||
:class:`telegram.error.TelegramError`
|
||||
"""
|
||||
# TODO: Remove when stability policy allows, tags: deprecated 21.11
|
||||
# also we should raise a deprecation warnung if anything is passed by
|
||||
# position since it will be moved, not sure how
|
||||
if gift_id is None:
|
||||
raise TypeError("Missing required argument `gift_id`.")
|
||||
data: JSONDict = {
|
||||
"user_id": user_id,
|
||||
"gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id,
|
||||
@@ -9924,7 +9883,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
"text_parse_mode": text_parse_mode,
|
||||
"text_entities": text_entities,
|
||||
"pay_for_upgrade": pay_for_upgrade,
|
||||
"chat_id": chat_id,
|
||||
}
|
||||
return await self._post(
|
||||
"sendGift",
|
||||
@@ -9947,7 +9905,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Verifies a chat |org-verify| which is represented by the bot.
|
||||
"""Verifies a chat on behalf of the organization which is represented by the bot.
|
||||
|
||||
.. versionadded:: 21.10
|
||||
|
||||
@@ -9989,7 +9947,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Verifies a user |org-verify| which is represented by the bot.
|
||||
"""Verifies a user on behalf of the organization which is represented by the bot.
|
||||
|
||||
.. versionadded:: 21.10
|
||||
|
||||
@@ -10030,8 +9988,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Removes verification from a chat that is currently verified |org-verify|
|
||||
represented by the bot.
|
||||
"""Removes verification from a chat that is currently verified on behalf of the
|
||||
organization represented by the bot.
|
||||
|
||||
|
||||
|
||||
@@ -10069,8 +10027,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Removes verification from a user who is currently verified |org-verify|
|
||||
represented by the bot.
|
||||
"""Removes verification from a user who is currently verified on behalf of the
|
||||
organization represented by the bot.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -831,7 +831,6 @@ class CallbackQuery(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
@@ -865,7 +864,6 @@ class CallbackQuery(TelegramObject):
|
||||
chat_id=chat_id,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
|
||||
+3
-23
@@ -1576,9 +1576,9 @@ class _ChatBase(TelegramObject):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -1940,8 +1940,6 @@ class _ChatBase(TelegramObject):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1980,8 +1978,6 @@ class _ChatBase(TelegramObject):
|
||||
parse_mode=parse_mode,
|
||||
supports_streaming=supports_streaming,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
api_kwargs=api_kwargs,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
caption_entities=caption_entities,
|
||||
@@ -2203,7 +2199,6 @@ class _ChatBase(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -2230,7 +2225,6 @@ class _ChatBase(TelegramObject):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -2263,7 +2257,6 @@ class _ChatBase(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -2290,7 +2283,6 @@ class _ChatBase(TelegramObject):
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -2406,7 +2398,6 @@ class _ChatBase(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2432,7 +2423,6 @@ class _ChatBase(TelegramObject):
|
||||
chat_id=self.id,
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -2450,7 +2440,6 @@ class _ChatBase(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2477,7 +2466,6 @@ class _ChatBase(TelegramObject):
|
||||
from_chat_id=self.id,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -3474,25 +3462,18 @@ class _ChatBase(TelegramObject):
|
||||
|
||||
await bot.send_gift(user_id=update.effective_chat.id, *args, **kwargs )
|
||||
|
||||
or::
|
||||
|
||||
await bot.send_gift(chat_id=update.effective_chat.id, *args, **kwargs )
|
||||
|
||||
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
|
||||
|
||||
Caution:
|
||||
Will only work if the chat is a private or channel chat, see :attr:`type`.
|
||||
Can only work, if the chat is a private chat, see :attr:`type`.
|
||||
|
||||
.. versionadded:: 21.8
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
|
||||
Added support for channel chats.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
"""
|
||||
return await self.get_bot().send_gift(
|
||||
user_id=self.id,
|
||||
gift_id=gift_id,
|
||||
text=text,
|
||||
text_parse_mode=text_parse_mode,
|
||||
@@ -3503,7 +3484,6 @@ class _ChatBase(TelegramObject):
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
**{"chat_id" if self.type == Chat.CHANNEL else "user_id": self.id},
|
||||
)
|
||||
|
||||
async def verify(
|
||||
|
||||
@@ -388,8 +388,8 @@ class BackgroundTypeWallpaper(BackgroundType):
|
||||
|
||||
class BackgroundTypePattern(BackgroundType):
|
||||
"""
|
||||
The background is a ``.PNG`` or ``.TGV`` (gzipped subset of ``SVG`` with ``MIME`` type
|
||||
``"application/x-tgwallpattern"``) pattern to be combined with the background fill
|
||||
The background is a `PNG` or `TGV` (gzipped subset of `SVG` with `MIME` type
|
||||
`"application/x-tgwallpattern"`) pattern to be combined with the background fill
|
||||
chosen by the user.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
|
||||
@@ -200,9 +200,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
|
||||
|
||||
Attributes:
|
||||
id (:obj:`int`): Unique identifier for this chat.
|
||||
@@ -357,9 +354,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
|
||||
|
||||
.. _accent colors: https://core.telegram.org/bots/api#accent-colors
|
||||
.. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
|
||||
@@ -375,7 +369,6 @@ class ChatFullInfo(_ChatBase):
|
||||
"business_intro",
|
||||
"business_location",
|
||||
"business_opening_hours",
|
||||
"can_send_gift",
|
||||
"can_send_paid_media",
|
||||
"can_set_sticker_set",
|
||||
"custom_emoji_sticker_set_name",
|
||||
@@ -452,7 +445,6 @@ class ChatFullInfo(_ChatBase):
|
||||
linked_chat_id: Optional[int] = None,
|
||||
location: Optional[ChatLocation] = None,
|
||||
can_send_paid_media: Optional[bool] = None,
|
||||
can_send_gift: Optional[bool] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -518,7 +510,6 @@ class ChatFullInfo(_ChatBase):
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
|
||||
|
||||
@@ -24,7 +24,6 @@ from typing import TYPE_CHECKING, Final, Optional
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
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
|
||||
@@ -100,7 +99,7 @@ class ChatMember(TelegramObject):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
# Required by all subclasses
|
||||
self.user: User = user
|
||||
self.status: str = enum.get_member(constants.ChatMemberStatus, status, status)
|
||||
self.status: str = status
|
||||
|
||||
self._id_attrs = (self.user, self.status)
|
||||
|
||||
|
||||
@@ -214,13 +214,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
Lastly you can pass an existing :class:`telegram.Video` object to send.
|
||||
thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): |thumbdocstringnopath|
|
||||
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): Cover for the video in the message. |fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
width (:obj:`int`, optional): Video width.
|
||||
height (:obj:`int`, optional): Video height.
|
||||
duration (:obj:`int`, optional): Video duration in seconds.
|
||||
@@ -232,13 +225,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
:tg-const:`telegram.constants.InputPaidMediaType.VIDEO`.
|
||||
media (:obj:`str` | :class:`telegram.InputFile`): Video to send.
|
||||
thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase|
|
||||
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|
||||
|fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
width (:obj:`int`): Optional. Video width.
|
||||
height (:obj:`int`): Optional. Video height.
|
||||
duration (:obj:`int`): Optional. Video duration in seconds.
|
||||
@@ -246,15 +232,7 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
suitable for streaming.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"cover",
|
||||
"duration",
|
||||
"height",
|
||||
"start_timestamp",
|
||||
"supports_streaming",
|
||||
"thumbnail",
|
||||
"width",
|
||||
)
|
||||
__slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -264,8 +242,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
height: Optional[int] = None,
|
||||
duration: Optional[int] = None,
|
||||
supports_streaming: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -288,10 +264,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
self.height: Optional[int] = height
|
||||
self.duration: Optional[int] = duration
|
||||
self.supports_streaming: Optional[bool] = supports_streaming
|
||||
self.cover: Optional[Union[InputFile, str]] = (
|
||||
parse_file_input(cover, attach=True, local_mode=True) if cover else None
|
||||
)
|
||||
self.start_timestamp: Optional[int] = start_timestamp
|
||||
|
||||
|
||||
class InputMediaAnimation(InputMedia):
|
||||
@@ -564,13 +536,6 @@ class InputMediaVideo(InputMedia):
|
||||
optional): |thumbdocstringnopath|
|
||||
|
||||
.. versionadded:: 20.2
|
||||
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): Cover for the video in the message. |fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
|
||||
|
||||
.. versionadded:: 21.3
|
||||
@@ -603,22 +568,13 @@ class InputMediaVideo(InputMedia):
|
||||
show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med|
|
||||
|
||||
.. versionadded:: 21.3
|
||||
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|
||||
|fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"cover",
|
||||
"duration",
|
||||
"has_spoiler",
|
||||
"height",
|
||||
"show_caption_above_media",
|
||||
"start_timestamp",
|
||||
"supports_streaming",
|
||||
"thumbnail",
|
||||
"width",
|
||||
@@ -638,8 +594,6 @@ class InputMediaVideo(InputMedia):
|
||||
has_spoiler: Optional[bool] = None,
|
||||
thumbnail: Optional[FileInput] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -671,10 +625,6 @@ class InputMediaVideo(InputMedia):
|
||||
self.supports_streaming: Optional[bool] = supports_streaming
|
||||
self.has_spoiler: Optional[bool] = has_spoiler
|
||||
self.show_caption_above_media: Optional[bool] = show_caption_above_media
|
||||
self.cover: Optional[Union[InputFile, str]] = (
|
||||
parse_file_input(cover, attach=True, local_mode=True) if cover else None
|
||||
)
|
||||
self.start_timestamp: Optional[int] = start_timestamp
|
||||
|
||||
|
||||
class InputMediaAudio(InputMedia):
|
||||
|
||||
@@ -61,8 +61,8 @@ class InputSticker(TelegramObject):
|
||||
format (:obj:`str`): Format of the added sticker, must be one of
|
||||
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
|
||||
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
|
||||
``.WEBM`` video.
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
|
||||
video.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
@@ -84,8 +84,8 @@ class InputSticker(TelegramObject):
|
||||
format (:obj:`str`): Format of the added sticker, must be one of
|
||||
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
|
||||
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
|
||||
``.WEBM`` video.
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
|
||||
video.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
"""
|
||||
|
||||
@@ -17,17 +17,12 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram Video."""
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import Optional
|
||||
|
||||
from telegram._files._basethumbedmedium import _BaseThumbedMedium
|
||||
from telegram._files.photosize import PhotoSize
|
||||
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
|
||||
class Video(_BaseThumbedMedium):
|
||||
"""This object represents a video file.
|
||||
@@ -53,13 +48,6 @@ class Video(_BaseThumbedMedium):
|
||||
thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail.
|
||||
|
||||
.. versionadded:: 20.2
|
||||
cover (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the cover of
|
||||
the video in the message.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Timestamp in seconds from which the video
|
||||
will play in the message
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Attributes:
|
||||
file_id (:obj:`str`): Identifier for this file, which can be used to download
|
||||
@@ -76,24 +64,9 @@ class Video(_BaseThumbedMedium):
|
||||
thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
|
||||
|
||||
.. versionadded:: 20.2
|
||||
cover (tuple[:class:`telegram.PhotoSize`]): Optional, Available sizes of the cover of
|
||||
the video in the message.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
start_timestamp (:obj:`int`): Optional, Timestamp in seconds from which the video
|
||||
will play in the message
|
||||
.. versionadded:: 21.11
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"cover",
|
||||
"duration",
|
||||
"file_name",
|
||||
"height",
|
||||
"mime_type",
|
||||
"start_timestamp",
|
||||
"width",
|
||||
)
|
||||
__slots__ = ("duration", "file_name", "height", "mime_type", "width")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -106,8 +79,6 @@ class Video(_BaseThumbedMedium):
|
||||
file_size: Optional[int] = None,
|
||||
file_name: Optional[str] = None,
|
||||
thumbnail: Optional[PhotoSize] = None,
|
||||
cover: Optional[Sequence[PhotoSize]] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -126,14 +97,3 @@ class Video(_BaseThumbedMedium):
|
||||
# Optional
|
||||
self.mime_type: Optional[str] = mime_type
|
||||
self.file_name: Optional[str] = file_name
|
||||
self.cover: Optional[Sequence[PhotoSize]] = parse_sequence_arg(cover)
|
||||
self.start_timestamp: Optional[int] = start_timestamp
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Video":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
data["cover"] = de_list_optional(data.get("cover"), PhotoSize, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -23,7 +23,9 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
|
||||
from telegram._inline.inlinequeryresult import InlineQueryResult
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.constants import InlineQueryResultType
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import InputMessageContent
|
||||
@@ -38,9 +40,6 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
.. versionchanged:: 20.5
|
||||
Removed the deprecated arguments and attributes ``thumb_*``.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Removed the deprecated argument and attribute ``hide_url``.
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
@@ -51,9 +50,12 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
|
||||
to the message.
|
||||
url (:obj:`str`, optional): URL of the result.
|
||||
hide_url (:obj:`bool`, optional): Pass :obj:`True`, if you don't want the URL to be shown
|
||||
in the message.
|
||||
|
||||
Tip:
|
||||
Pass an empty string as URL if you don't want the URL to be shown in the message.
|
||||
.. deprecated:: 21.10
|
||||
This attribute will be removed in future PTB versions. Pass an empty string as URL
|
||||
instead.
|
||||
description (:obj:`str`, optional): Short description of the result.
|
||||
thumbnail_url (:obj:`str`, optional): Url of the thumbnail for the result.
|
||||
|
||||
@@ -76,6 +78,12 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
|
||||
to the message.
|
||||
url (:obj:`str`): Optional. URL of the result.
|
||||
hide_url (:obj:`bool`): Optional. Pass :obj:`True`, if you don't want the URL to be shown
|
||||
in the message.
|
||||
|
||||
.. deprecated:: 21.10
|
||||
This attribute will be removed in future PTB versions. Pass an empty string as URL
|
||||
instead.
|
||||
description (:obj:`str`): Optional. Short description of the result.
|
||||
thumbnail_url (:obj:`str`): Optional. Url of the thumbnail for the result.
|
||||
|
||||
@@ -91,6 +99,7 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
|
||||
__slots__ = (
|
||||
"description",
|
||||
"hide_url",
|
||||
"input_message_content",
|
||||
"reply_markup",
|
||||
"thumbnail_height",
|
||||
@@ -107,6 +116,7 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
input_message_content: "InputMessageContent",
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||||
url: Optional[str] = None,
|
||||
hide_url: Optional[bool] = None,
|
||||
description: Optional[str] = None,
|
||||
thumbnail_url: Optional[str] = None,
|
||||
thumbnail_width: Optional[int] = None,
|
||||
@@ -123,6 +133,16 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
# Optional
|
||||
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
|
||||
self.url: Optional[str] = url
|
||||
if hide_url is not None:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"21.10",
|
||||
"The argument `hide_url` will be removed in future PTB"
|
||||
"versions. Pass an empty string as URL instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self.hide_url: Optional[bool] = hide_url
|
||||
self.description: Optional[str] = description
|
||||
self.thumbnail_url: Optional[str] = thumbnail_url
|
||||
self.thumbnail_width: Optional[int] = thumbnail_width
|
||||
|
||||
@@ -47,7 +47,7 @@ class InlineQueryResultGif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
|
||||
gif_width (:obj:`int`, optional): Width of the GIF.
|
||||
gif_height (:obj:`int`, optional): Height of the GIF.
|
||||
gif_duration (:obj:`int`, optional): Duration of the GIF in seconds.
|
||||
@@ -86,7 +86,7 @@ class InlineQueryResultGif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
|
||||
gif_width (:obj:`int`): Optional. Width of the GIF.
|
||||
gif_height (:obj:`int`): Optional. Height of the GIF.
|
||||
gif_duration (:obj:`int`): Optional. Duration of the GIF in seconds.
|
||||
|
||||
@@ -48,7 +48,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
|
||||
mpeg4_width (:obj:`int`, optional): Video width.
|
||||
mpeg4_height (:obj:`int`, optional): Video height.
|
||||
mpeg4_duration (:obj:`int`, optional): Video duration in seconds.
|
||||
@@ -88,7 +88,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
|
||||
mpeg4_width (:obj:`int`): Optional. Video width.
|
||||
mpeg4_height (:obj:`int`): Optional. Video height.
|
||||
mpeg4_duration (:obj:`int`): Optional. Video duration in seconds.
|
||||
|
||||
@@ -35,11 +35,9 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`title`, :attr:`description`, :attr:`payload`,
|
||||
:attr:`currency` and :attr:`prices` are equal.
|
||||
:attr:`provider_token`, :attr:`currency` and :attr:`prices` are equal.
|
||||
|
||||
.. versionadded:: 13.5
|
||||
.. versionchanged:: 21.11
|
||||
:attr:`provider_token` is no longer considered for equality comparison.
|
||||
|
||||
Args:
|
||||
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
|
||||
@@ -51,13 +49,13 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
|
||||
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed
|
||||
to the user, use it for your internal processes.
|
||||
provider_token (:obj:`str`, optional): Payment provider token, obtained via
|
||||
provider_token (:obj:`str`): Payment provider token, obtained via
|
||||
`@Botfather <https://t.me/Botfather>`_. Pass an empty string for payments in
|
||||
|tg_stars|.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Bot API 7.4 made this parameter is optional and this is now reflected in the
|
||||
class signature.
|
||||
.. deprecated:: 21.3
|
||||
As of Bot API 7.4, this parameter is now optional and future versions of the
|
||||
library will make it optional as well.
|
||||
currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on
|
||||
`currencies <https://core.telegram.org/bots/payments#supported-currencies>`_.
|
||||
Pass ``XTR`` for payments in |tg_stars|.
|
||||
@@ -201,9 +199,9 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str], # This arg is now optional since Bot API 7.4
|
||||
currency: str,
|
||||
prices: Sequence[LabeledPrice],
|
||||
provider_token: Optional[str] = None,
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[Sequence[int]] = None,
|
||||
provider_data: Optional[str] = None,
|
||||
@@ -227,10 +225,10 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
self.title: str = title
|
||||
self.description: str = description
|
||||
self.payload: str = payload
|
||||
self.provider_token: Optional[str] = provider_token
|
||||
self.currency: str = currency
|
||||
self.prices: tuple[LabeledPrice, ...] = parse_sequence_arg(prices)
|
||||
# Optionals
|
||||
self.provider_token: Optional[str] = provider_token
|
||||
self.max_tip_amount: Optional[int] = max_tip_amount
|
||||
self.suggested_tip_amounts: tuple[int, ...] = parse_sequence_arg(suggested_tip_amounts)
|
||||
self.provider_data: Optional[str] = provider_data
|
||||
@@ -250,6 +248,7 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
self.provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
)
|
||||
|
||||
+1
-11
@@ -2592,8 +2592,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -2663,8 +2661,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
message_thread_id=message_thread_id,
|
||||
has_spoiler=has_spoiler,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
business_connection_id=self.business_connection_id,
|
||||
message_effect_id=message_effect_id,
|
||||
allow_paid_broadcast=allow_paid_broadcast,
|
||||
@@ -3386,9 +3382,9 @@ class Message(MaybeInaccessibleMessage):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -3510,7 +3506,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3545,7 +3540,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
chat_id=chat_id,
|
||||
from_chat_id=self.chat_id,
|
||||
message_id=self.message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
message_thread_id=message_thread_id,
|
||||
@@ -3569,7 +3563,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -3600,7 +3593,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
from_chat_id=self.chat_id,
|
||||
message_id=self.message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -3633,7 +3625,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -3684,7 +3675,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
|
||||
@@ -36,9 +36,6 @@ if TYPE_CHECKING:
|
||||
|
||||
class StarTransaction(TelegramObject):
|
||||
"""Describes a Telegram Star transaction.
|
||||
Note that if the buyer initiates a chargeback with the payment provider from whom they
|
||||
acquired Stars (e.g., Apple, Google) following this transaction, the refunded Stars will be
|
||||
deducted from the bot's balance. This is outside of Telegram's control.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`id`, :attr:`source`, and :attr:`receiver` are equal.
|
||||
|
||||
@@ -23,7 +23,6 @@ from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Final, Optional
|
||||
|
||||
from telegram import constants
|
||||
from telegram._chat import Chat
|
||||
from telegram._gifts import Gift
|
||||
from telegram._paidmedia import PaidMedia
|
||||
from telegram._telegramobject import TelegramObject
|
||||
@@ -44,7 +43,6 @@ class TransactionPartner(TelegramObject):
|
||||
transactions. Currently, it can be one of:
|
||||
|
||||
* :class:`TransactionPartnerUser`
|
||||
* :class:`TransactionPartnerChat`
|
||||
* :class:`TransactionPartnerAffiliateProgram`
|
||||
* :class:`TransactionPartnerFragment`
|
||||
* :class:`TransactionPartnerTelegramAds`
|
||||
@@ -56,9 +54,6 @@ class TransactionPartner(TelegramObject):
|
||||
|
||||
.. versionadded:: 21.4
|
||||
|
||||
..versionchanged:: 21.11
|
||||
Added :class:`TransactionPartnerChat`
|
||||
|
||||
Args:
|
||||
type (:obj:`str`): The type of the transaction partner.
|
||||
|
||||
@@ -73,11 +68,6 @@ class TransactionPartner(TelegramObject):
|
||||
|
||||
.. versionadded:: 21.9
|
||||
"""
|
||||
CHAT: Final[str] = constants.TransactionPartnerType.CHAT
|
||||
""":const:`telegram.constants.TransactionPartnerType.CHAT`
|
||||
|
||||
.. versionadded:: 21.11
|
||||
"""
|
||||
FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
|
||||
""":const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
|
||||
OTHER: Final[str] = constants.TransactionPartnerType.OTHER
|
||||
@@ -113,7 +103,6 @@ class TransactionPartner(TelegramObject):
|
||||
|
||||
_class_mapping: dict[str, type[TransactionPartner]] = {
|
||||
cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
|
||||
cls.CHAT: TransactionPartnerChat,
|
||||
cls.FRAGMENT: TransactionPartnerFragment,
|
||||
cls.USER: TransactionPartnerUser,
|
||||
cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
|
||||
@@ -182,60 +171,6 @@ class TransactionPartnerAffiliateProgram(TransactionPartner):
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
class TransactionPartnerChat(TransactionPartner):
|
||||
"""Describes a transaction with a chat.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`chat` are equal.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Args:
|
||||
chat (:class:`telegram.Chat`): Information about the chat.
|
||||
gift (:class:`telegram.Gift`, optional): The gift sent to the chat by the bot.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): The type of the transaction partner,
|
||||
always :tg-const:`telegram.TransactionPartner.CHAT`.
|
||||
chat (:class:`telegram.Chat`): Information about the chat.
|
||||
gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"chat",
|
||||
"gift",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chat: Chat,
|
||||
gift: Optional[Gift] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> None:
|
||||
super().__init__(type=TransactionPartner.CHAT, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
self.chat: Chat = chat
|
||||
self.gift: Optional[Gift] = gift
|
||||
|
||||
self._id_attrs = (
|
||||
self.type,
|
||||
self.chat,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartnerChat":
|
||||
"""See :meth:`telegram.TransactionPartner.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["gift"] = de_json_optional(data.get("gift"), Gift, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
class TransactionPartnerFragment(TransactionPartner):
|
||||
"""Describes a withdrawal transaction with Fragment.
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ if TYPE_CHECKING:
|
||||
|
||||
class SuccessfulPayment(TelegramObject):
|
||||
"""This object contains basic information about a successful payment.
|
||||
Note that if the buyer initiates a chargeback with the relevant payment provider following
|
||||
this transaction, the funds may be debited from your balance. This is outside of
|
||||
Telegram's control.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`telegram_payment_charge_id` and
|
||||
|
||||
+2
-15
@@ -1018,9 +1018,9 @@ class User(TelegramObject):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -1328,8 +1328,6 @@ class User(TelegramObject):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1371,8 +1369,6 @@ class User(TelegramObject):
|
||||
parse_mode=parse_mode,
|
||||
supports_streaming=supports_streaming,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
api_kwargs=api_kwargs,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
caption_entities=caption_entities,
|
||||
@@ -1674,7 +1670,7 @@ class User(TelegramObject):
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
await bot.send_gift(user_id=update.effective_user.id, *args, **kwargs )
|
||||
await bot.send_gift( user_id=update.effective_user.id, *args, **kwargs )
|
||||
|
||||
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
|
||||
|
||||
@@ -1684,7 +1680,6 @@ class User(TelegramObject):
|
||||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
"""
|
||||
return await self.get_bot().send_gift(
|
||||
chat_id=None,
|
||||
user_id=self.id,
|
||||
gift_id=gift_id,
|
||||
text=text,
|
||||
@@ -1712,7 +1707,6 @@ class User(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1740,7 +1734,6 @@ class User(TelegramObject):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -1773,7 +1766,6 @@ class User(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1801,7 +1793,6 @@ class User(TelegramObject):
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -1917,7 +1908,6 @@ class User(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1943,7 +1933,6 @@ class User(TelegramObject):
|
||||
chat_id=self.id,
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -1961,7 +1950,6 @@ class User(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1988,7 +1976,6 @@ class User(TelegramObject):
|
||||
from_chat_id=self.id,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
|
||||
@@ -51,6 +51,6 @@ class Version(NamedTuple):
|
||||
|
||||
|
||||
__version_info__: Final[Version] = Version(
|
||||
major=21, minor=11, micro=0, releaselevel="final", serial=0
|
||||
major=21, minor=10, micro=0, releaselevel="final", serial=0
|
||||
)
|
||||
__version__: Final[str] = str(__version_info__)
|
||||
|
||||
+5
-13
@@ -155,7 +155,7 @@ class _AccentColor(NamedTuple):
|
||||
#: :data:`telegram.__bot_api_version_info__`.
|
||||
#:
|
||||
#: .. versionadded:: 20.0
|
||||
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=3)
|
||||
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=2)
|
||||
#: :obj:`str`: Telegram Bot API
|
||||
#: version supported by this version of `python-telegram-bot`. Also available as
|
||||
#: :data:`telegram.__bot_api_version__`.
|
||||
@@ -1236,12 +1236,9 @@ class GiftLimit(IntEnum):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
MAX_TEXT_LENGTH = 128
|
||||
MAX_TEXT_LENGTH = 255
|
||||
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.Bot.send_gift.text` parameter of :meth:`~telegram.Bot.send_gift`.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Updated Value to 128 based on Bot API 8.3
|
||||
"""
|
||||
|
||||
|
||||
@@ -2621,13 +2618,13 @@ class StickerSetLimit(IntEnum):
|
||||
:meth:`telegram.Bot.add_sticker_to_set`.
|
||||
"""
|
||||
MAX_STATIC_THUMBNAIL_SIZE = 128
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a ``.WEBP`` or ``.PNG`` in kilobytes,
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a **.WEBP** or **.PNG** in kilobytes,
|
||||
as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
|
||||
MAX_ANIMATED_THUMBNAIL_SIZE = 32
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a ``.TGS`` or ``.WEBM`` in kilobytes,
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a **.TGS** or **.WEBM** in kilobytes,
|
||||
as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
|
||||
STATIC_THUMB_DIMENSIONS = 100
|
||||
""":obj:`int`: Exact height and width of the thumbnail if it is a ``.WEBP`` or ``.PNG`` in
|
||||
""":obj:`int`: Exact height and width of the thumbnail if it is a **.WEBP** or **.PNG** in
|
||||
pixels, as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
|
||||
|
||||
|
||||
@@ -2662,11 +2659,6 @@ class TransactionPartnerType(StringEnum):
|
||||
|
||||
.. versionadded:: 21.9
|
||||
"""
|
||||
CHAT = "chat"
|
||||
""":obj:`str`: Transaction with a chat.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
"""
|
||||
FRAGMENT = "fragment"
|
||||
""":obj:`str`: Withdrawal transaction with Fragment."""
|
||||
OTHER = "other"
|
||||
|
||||
@@ -42,6 +42,7 @@ __all__ = (
|
||||
"Defaults",
|
||||
"DictPersistence",
|
||||
"ExtBot",
|
||||
"FiniteStateMachine",
|
||||
"InlineQueryHandler",
|
||||
"InvalidCallbackData",
|
||||
"Job",
|
||||
@@ -57,6 +58,9 @@ __all__ = (
|
||||
"PrefixHandler",
|
||||
"ShippingQueryHandler",
|
||||
"SimpleUpdateProcessor",
|
||||
"SingleStateMachine",
|
||||
"State",
|
||||
"StateInfo",
|
||||
"StringCommandHandler",
|
||||
"StringRegexHandler",
|
||||
"TypeHandler",
|
||||
@@ -77,6 +81,7 @@ from ._contexttypes import ContextTypes
|
||||
from ._defaults import Defaults
|
||||
from ._dictpersistence import DictPersistence
|
||||
from ._extbot import ExtBot
|
||||
from ._fsm import FiniteStateMachine, SingleStateMachine, State, StateInfo
|
||||
from ._handlers.basehandler import BaseHandler
|
||||
from ._handlers.businessconnectionhandler import BusinessConnectionHandler
|
||||
from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler
|
||||
|
||||
@@ -98,7 +98,7 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
:tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second by
|
||||
paying a fee in Telegram Stars.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
This class automatically takes the
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` parameter into account and
|
||||
throttles the requests accordingly.
|
||||
|
||||
@@ -48,9 +48,9 @@ from telegram.error import TelegramError
|
||||
from telegram.ext._basepersistence import BasePersistence
|
||||
from telegram.ext._contexttypes import ContextTypes
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._fsm import SingleStateMachine, State, StateInfo
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._updater import Updater
|
||||
from telegram.ext._utils.networkloop import network_retry_loop
|
||||
from telegram.ext._utils.stack import was_called_by
|
||||
from telegram.ext._utils.trackingdict import TrackingDict
|
||||
from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, RT, UD, ConversationKey, HandlerCallback
|
||||
@@ -60,7 +60,7 @@ if TYPE_CHECKING:
|
||||
from socket import socket
|
||||
|
||||
from telegram import Message
|
||||
from telegram.ext import ConversationHandler, JobQueue
|
||||
from telegram.ext import ConversationHandler, FiniteStateMachine, JobQueue
|
||||
from telegram.ext._applicationbuilder import InitApplicationBuilder
|
||||
from telegram.ext._baseupdateprocessor import BaseUpdateProcessor
|
||||
from telegram.ext._jobqueue import Job
|
||||
@@ -267,6 +267,7 @@ class Application(
|
||||
"update_queue",
|
||||
"updater",
|
||||
"user_data",
|
||||
"fsm",
|
||||
)
|
||||
# Allowing '__weakref__' creation here since we need it for the JobQueue
|
||||
# Currently the __weakref__ slot is already created
|
||||
@@ -302,11 +303,12 @@ class Application(
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self.fsm: FiniteStateMachine = SingleStateMachine()
|
||||
self.bot: BT = bot
|
||||
self.update_queue: asyncio.Queue[object] = update_queue
|
||||
self.context_types: ContextTypes[CCT, UD, CD, BD] = context_types
|
||||
self.updater: Optional[Updater] = updater
|
||||
self.handlers: dict[int, list[BaseHandler[Any, CCT, Any]]] = {}
|
||||
self.handlers: dict[State, dict[int, list[BaseHandler[Any, CCT, Any]]]] = {}
|
||||
self.error_handlers: dict[
|
||||
HandlerCallback[object, CCT, None], Union[bool, DefaultValue[bool]]
|
||||
] = {}
|
||||
@@ -740,7 +742,7 @@ class Application(
|
||||
self,
|
||||
poll_interval: float = 0.0,
|
||||
timeout: int = 10,
|
||||
bootstrap_retries: int = 0,
|
||||
bootstrap_retries: int = -1,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -781,19 +783,13 @@ class Application(
|
||||
Telegram in seconds. Default is ``0.0``.
|
||||
timeout (:obj:`int`, optional): Passed to
|
||||
:paramref:`telegram.Bot.get_updates.timeout`. Default is ``10`` seconds.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase
|
||||
(calling :meth:`initialize` and the boostrapping of
|
||||
:meth:`telegram.ext.Updater.start_polling`)
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
* < 0 - retry indefinitely (default)
|
||||
* 0 - no retries
|
||||
* > 0 - retry up to X times
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
The default value will be changed to from ``-1`` to ``0``. Indefinite retries
|
||||
during bootstrapping are not recommended.
|
||||
|
||||
read_timeout (:obj:`float`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
@@ -883,9 +879,8 @@ class Application(
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
error_callback=error_callback, # if there is an error in fetching updates
|
||||
),
|
||||
stop_signals=stop_signals,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
close_loop=close_loop,
|
||||
stop_signals=stop_signals,
|
||||
)
|
||||
|
||||
def run_webhook(
|
||||
@@ -954,10 +949,8 @@ class Application(
|
||||
url_path (:obj:`str`, optional): Path inside url. Defaults to `` '' ``
|
||||
cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file.
|
||||
key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase
|
||||
(calling :meth:`initialize` and the boostrapping of
|
||||
:meth:`telegram.ext.Updater.start_polling`)
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
@@ -1043,28 +1036,18 @@ class Application(
|
||||
secret_token=secret_token,
|
||||
unix=unix,
|
||||
),
|
||||
stop_signals=stop_signals,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
close_loop=close_loop,
|
||||
)
|
||||
|
||||
async def _bootstrap_initialize(self, max_retries: int) -> None:
|
||||
await network_retry_loop(
|
||||
action_cb=self.initialize,
|
||||
description="Bootstrap Initialize Application",
|
||||
max_retries=max_retries,
|
||||
interval=1,
|
||||
stop_signals=stop_signals,
|
||||
)
|
||||
|
||||
def __run(
|
||||
self,
|
||||
updater_coroutine: Coroutine,
|
||||
stop_signals: ODVInput[Sequence[int]],
|
||||
bootstrap_retries: int,
|
||||
close_loop: bool = True,
|
||||
) -> None:
|
||||
# Calling get_event_loop() should still be okay even in py3.10+ as long as there is a
|
||||
# running event loop, or we are in the main thread, which are the intended use cases.
|
||||
# running event loop or we are in the main thread, which are the intended use cases.
|
||||
# See the docs of get_event_loop() and get_running_loop() for more info
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
@@ -1084,7 +1067,7 @@ class Application(
|
||||
)
|
||||
|
||||
try:
|
||||
loop.run_until_complete(self._bootstrap_initialize(max_retries=bootstrap_retries))
|
||||
loop.run_until_complete(self.initialize())
|
||||
if self.post_init:
|
||||
loop.run_until_complete(self.post_init(self))
|
||||
if self.__stop_running_marker.is_set():
|
||||
@@ -1298,19 +1281,46 @@ class Application(
|
||||
# Processing updates before initialize() is a problem e.g. if persistence is used
|
||||
self._check_initialized()
|
||||
|
||||
fsm_state_info = self.fsm.get_state_info(update)
|
||||
|
||||
for state, state_handlers in self.handlers.items():
|
||||
if state.matches(fsm_state_info.state):
|
||||
_LOGGER.debug("Processing in state %s", state)
|
||||
was_handled = await self.__process_update_groups(
|
||||
update, state_handlers, fsm_state_info
|
||||
)
|
||||
if was_handled:
|
||||
_LOGGER.debug(
|
||||
"Update was handled in state %s. Stopping further processing", state
|
||||
)
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"No handlers found for key %s in state %s", fsm_state_info.key, fsm_state_info.state
|
||||
)
|
||||
return
|
||||
|
||||
async def __process_update_groups(
|
||||
self,
|
||||
update: object,
|
||||
state_handlers: dict[int, list[BaseHandler]],
|
||||
fsm_state_info: StateInfo,
|
||||
) -> bool:
|
||||
context = None
|
||||
was_handled = False
|
||||
any_blocking = False # Flag which is set to True if any handler specifies block=True
|
||||
|
||||
for handlers in self.handlers.values():
|
||||
for handlers in state_handlers.values():
|
||||
try:
|
||||
for handler in handlers:
|
||||
check = handler.check_update(update) # Should the handler handle this update?
|
||||
if check is None or check is False:
|
||||
continue
|
||||
was_handled = True
|
||||
|
||||
if not context: # build a context if not already built
|
||||
try:
|
||||
context = self.context_types.context.from_update(update, self)
|
||||
context.fsm_state_info = fsm_state_info
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
(
|
||||
@@ -1320,7 +1330,7 @@ class Application(
|
||||
update,
|
||||
exc_info=exc,
|
||||
)
|
||||
return
|
||||
return True
|
||||
await context.refresh_data()
|
||||
coroutine: Coroutine = handler.handle_update(update, self, check, context)
|
||||
|
||||
@@ -1360,7 +1370,14 @@ class Application(
|
||||
# (in __create_task_callback)
|
||||
self._mark_for_persistence_update(update=update)
|
||||
|
||||
def add_handler(self, handler: BaseHandler[Any, CCT, Any], group: int = DEFAULT_GROUP) -> None:
|
||||
return was_handled
|
||||
|
||||
def add_handler(
|
||||
self,
|
||||
handler: BaseHandler[Any, CCT, Any],
|
||||
group: int = DEFAULT_GROUP,
|
||||
state: State = State.IDLE,
|
||||
) -> None:
|
||||
"""Register a handler.
|
||||
|
||||
TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
|
||||
@@ -1419,11 +1436,11 @@ class Application(
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if group not in self.handlers:
|
||||
self.handlers[group] = []
|
||||
self.handlers = dict(sorted(self.handlers.items())) # lower -> higher groups
|
||||
state_handlers = self.handlers.setdefault(state, {})
|
||||
if group not in state_handlers:
|
||||
state_handlers[group] = []
|
||||
|
||||
self.handlers[group].append(handler)
|
||||
state_handlers[group].append(handler)
|
||||
|
||||
def add_handlers(
|
||||
self,
|
||||
@@ -1495,10 +1512,11 @@ class Application(
|
||||
group (:obj:`object`, optional): The group identifier. Default is ``0``.
|
||||
|
||||
"""
|
||||
if handler in self.handlers[group]:
|
||||
self.handlers[group].remove(handler)
|
||||
if not self.handlers[group]:
|
||||
del self.handlers[group]
|
||||
for state_handlers in self.handlers.values():
|
||||
if handler in state_handlers[group]:
|
||||
state_handlers[group].remove(handler)
|
||||
if not state_handlers[group]:
|
||||
del state_handlers[group]
|
||||
|
||||
def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Drops the corresponding entry from the :attr:`chat_data`. Will also be deleted from
|
||||
|
||||
@@ -393,7 +393,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
.. seealso:: :paramref:`telegram.Bot.base_url`,
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_file_url`
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
|
||||
Args:
|
||||
@@ -415,7 +415,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
.. seealso:: :paramref:`telegram.Bot.base_file_url`,
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_url`
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -113,7 +113,7 @@ class BaseUpdateProcessor(AbstractAsyncContextManager["BaseUpdateProcessor"], AB
|
||||
This value is a snapshot of the current number of updates being processed. It may
|
||||
change immediately after being read.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
.. versionadded:: NEXT.VERSION
|
||||
"""
|
||||
return self.max_concurrent_updates - self._semaphore.current_value
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the CallbackContext class."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Generator
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from re import Match
|
||||
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, TypeVar, Union
|
||||
|
||||
@@ -26,12 +27,13 @@ from telegram._callbackquery import CallbackQuery
|
||||
from telegram._update import Update
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._fsm import FiniteStateMachine, State
|
||||
from telegram.ext._utils.types import BD, BT, CD, UD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncio import Future, Queue
|
||||
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
from telegram.ext import Application, Job, JobQueue, StateInfo
|
||||
from telegram.ext._utils.types import CCT
|
||||
|
||||
_STORING_DATA_WIKI = (
|
||||
@@ -121,6 +123,7 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"args",
|
||||
"coroutine",
|
||||
"error",
|
||||
"fsm_state_info",
|
||||
"job",
|
||||
"matches",
|
||||
)
|
||||
@@ -141,6 +144,7 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
self.coroutine: Optional[
|
||||
Union[Generator[Optional[Future[object]], None, Any], Awaitable[Any]]
|
||||
] = None
|
||||
self.fsm_state_info: StateInfo = None # type: ignore[assignment]
|
||||
|
||||
@property
|
||||
def application(self) -> "Application[BT, ST, UD, CD, BD, Any]":
|
||||
@@ -269,6 +273,22 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"telegram.Bot does not allow for arbitrary callback data."
|
||||
)
|
||||
|
||||
@property
|
||||
def fsm(self) -> FiniteStateMachine:
|
||||
return self.application.fsm
|
||||
|
||||
def fsm_semaphore(self) -> asyncio.Lock:
|
||||
return self.fsm.get_lock(self.fsm_state_info.key)
|
||||
|
||||
async def set_state(self, state: State) -> None:
|
||||
await self.fsm.set_state(self.fsm_state_info.key, state, self.fsm_state_info.version)
|
||||
|
||||
def set_state_nowait(self, state: State) -> None:
|
||||
self.fsm.set_state_nowait(self.fsm_state_info.key, state, self.fsm_state_info.version)
|
||||
|
||||
def as_fsm_state(self, state: State) -> AbstractAsyncContextManager[None]:
|
||||
return self.fsm.as_state(self.fsm_state_info.key, state)
|
||||
|
||||
@classmethod
|
||||
def from_error(
|
||||
cls: type["CCT"],
|
||||
|
||||
+4
-14
@@ -815,7 +815,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -832,7 +831,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -1197,9 +1195,9 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[Sequence[int]] = None,
|
||||
provider_data: Optional[Union[str, object]] = None,
|
||||
@@ -1754,7 +1752,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1767,7 +1764,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
chat_id=chat_id,
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
message_thread_id=message_thread_id,
|
||||
@@ -2772,9 +2768,9 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -3244,8 +3240,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -3276,8 +3270,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
business_connection_id=business_connection_id,
|
||||
has_spoiler=has_spoiler,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
filename=filename,
|
||||
reply_parameters=reply_parameters,
|
||||
read_timeout=read_timeout,
|
||||
@@ -4476,13 +4468,12 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
|
||||
async def send_gift(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
gift_id: Union[str, Gift] = None, # type: ignore
|
||||
user_id: int,
|
||||
gift_id: Union[str, Gift],
|
||||
text: Optional[str] = None,
|
||||
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
text_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
pay_for_upgrade: Optional[bool] = None,
|
||||
chat_id: Optional[Union[str, int]] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -4493,7 +4484,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
) -> bool:
|
||||
return await super().send_gift(
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
gift_id=gift_id,
|
||||
text=text,
|
||||
text_parse_mode=text_parse_mode,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Private Submbodule for finite state machine implementation."""
|
||||
|
||||
__all__ = ["FiniteStateMachine", "SingleStateMachine", "State", "StateInfo"]
|
||||
|
||||
from .machine import FiniteStateMachine, SingleStateMachine, StateInfo
|
||||
from .states import State
|
||||
@@ -0,0 +1,200 @@
|
||||
"""This Module contains the FiniteStateMachine class and the built-in subclass SingleStateMachine.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime as dtm
|
||||
import logging
|
||||
import time
|
||||
import weakref
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncIterator, Hashable, Mapping, MutableSequence, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Generic, Literal, Optional, TypeVar, Union, overload
|
||||
|
||||
from telegram.ext._fsm.states import State
|
||||
from telegram.ext._utils.types import JobCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
from telegram.ext import JobQueue
|
||||
|
||||
_KT = TypeVar("_KT", bound=Hashable)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class StateInfo(Generic[_KT]):
|
||||
def __init__(self: "StateInfo[_KT]", key: _KT, state: State, version: int) -> None:
|
||||
self.key: _KT = key
|
||||
self.state: State = state
|
||||
self.version: int = version
|
||||
|
||||
|
||||
class FiniteStateMachine(abc.ABC, Generic[_KT]):
|
||||
def __init__(self) -> None:
|
||||
self._locks: MutableMapping[_KT, asyncio.Lock] = weakref.WeakValueDictionary()
|
||||
|
||||
# There is likely litte benefit for a user to customize how exactly the states are stored
|
||||
# and accessed. So we make this private and only provide a read-only view.
|
||||
self.__states: dict[_KT, tuple[State, int]] = defaultdict(
|
||||
lambda: (State.IDLE, time.perf_counter_ns())
|
||||
)
|
||||
self._states = MappingProxyType(self.__states)
|
||||
|
||||
self.__job_queue: Optional[weakref.ReferenceType[JobQueue]] = None
|
||||
self.__history: Mapping[_KT, MutableSequence[State]] = defaultdict(
|
||||
lambda: deque(maxlen=10)
|
||||
)
|
||||
|
||||
@property
|
||||
def states(self) -> Mapping[_KT, tuple[State, int]]:
|
||||
return self._states
|
||||
|
||||
def store_state_history(self, key: _KT, state: State) -> None:
|
||||
# Making this public so that users can override if they want to customize the history
|
||||
# E.g., they could want to store more/fewer states, also depending on the key
|
||||
self.__history[key].append(state)
|
||||
|
||||
def get_state_history(self, key: _KT) -> Sequence[State]:
|
||||
return list(self.__history[key])
|
||||
|
||||
def get_lock(self, key: _KT) -> asyncio.Lock:
|
||||
"""Returns a lock that is unique for this key at runtime.
|
||||
It can be used to prevent concurrent access to resources associated to this key.
|
||||
"""
|
||||
return self._locks.setdefault(key, asyncio.Lock())
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_state_info(self, update: object) -> StateInfo[_KT]:
|
||||
"""Returns exactly one active state for the update.
|
||||
If more than one stored key applies to the update, one must be chosen.
|
||||
It's recommended to select the most specific one.
|
||||
|
||||
Example:
|
||||
The state of a chat, a user or a user in a specific chat could be tracked.
|
||||
For a message in that chat, the state of the user in that chat should be returned if
|
||||
available. Otherwise, the state of the chat should be returned.
|
||||
|
||||
Important:
|
||||
This must be an atomic operation and not e.g. wait for a lock.
|
||||
Instead, if necessary, return a special state indicating that the key is currently
|
||||
busy.
|
||||
"""
|
||||
|
||||
def _do_set_state(
|
||||
self, key: _KT, state: State, version: Optional[int] = None
|
||||
) -> StateInfo[_KT]:
|
||||
"""Protected method to set the state for the specified key.
|
||||
|
||||
The version can be optionally used for optimistic locking. If the version does not match
|
||||
the current version, the state should not be updated.
|
||||
|
||||
Important:
|
||||
This should be used exclusively by methods of this class and subclasses.
|
||||
It should *not* be called directly by users of this class!
|
||||
"""
|
||||
_LOGGER.debug("Setting %s state to %s", key, state)
|
||||
if state is State.ANY:
|
||||
raise ValueError("State.ANY is not supported in set_state")
|
||||
|
||||
if version and version != self._states.get(key, (None, None))[1]:
|
||||
raise ValueError("Optimistic locking failed. Not updating state.")
|
||||
|
||||
if jq := self._get_job_queue(raise_exception=False):
|
||||
# This is a rather tight coupling between FSM and JobQueue
|
||||
# Not sure if we like that. Makes it even harder to replace JobQueue
|
||||
# (or the JQ implementation) with something else.
|
||||
# The upside is that we don't need to maintain any additional internal state
|
||||
# for the jobs and persistence is handled by the JobQueue.
|
||||
cancel_jobs = jq.jobs(pattern=str(hash(key)))
|
||||
for job in cancel_jobs:
|
||||
_LOGGER.debug("Cancelling timeout job %s", job)
|
||||
job.schedule_removal()
|
||||
|
||||
# important to use time.perf_counter_ns() here, as time_ns() is not monotonic
|
||||
self.__states[key] = (state, time.perf_counter_ns())
|
||||
# Doing this *after* do_set_state so that any exceptions are raised before the history
|
||||
# is updated
|
||||
self.store_state_history(key, state)
|
||||
return StateInfo(key, state, self._states[key][1])
|
||||
|
||||
async def set_state(self, key: _KT, state: State, version: Optional[int] = None) -> None:
|
||||
"""Store the state for the specified key."""
|
||||
async with self.get_lock(key):
|
||||
self._do_set_state(key, state, version)
|
||||
|
||||
def set_state_nowait(self, key: _KT, state: State, version: Optional[int] = None) -> None:
|
||||
"""Store the state for the specified key without waiting for a lock."""
|
||||
if self.get_lock(key).locked():
|
||||
raise asyncio.InvalidStateError("Lock is locked")
|
||||
self._do_set_state(key, state, version)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def as_state(self, key: _KT, state: State) -> AsyncIterator[None]:
|
||||
"""Context manager to set the state for the specified key and reset it afterwards."""
|
||||
async with self.get_lock(key):
|
||||
current_state, current_version = self.states[key]
|
||||
new_version = self._do_set_state(key, state, current_version).version
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._do_set_state(key, current_state, new_version)
|
||||
|
||||
@staticmethod
|
||||
def _build_job_name(keys: Sequence[_KT]) -> str:
|
||||
return f"FSM_Job_{'_'.join(str(hash(k)) for k in keys)}"
|
||||
|
||||
def set_job_queue(self, job_queue: "JobQueue") -> None:
|
||||
self.__job_queue = weakref.ref(job_queue)
|
||||
|
||||
@overload
|
||||
def _get_job_queue(self, raise_exception: Literal[False]) -> Optional["JobQueue"]: ...
|
||||
|
||||
@overload
|
||||
def _get_job_queue(self) -> "JobQueue": ...
|
||||
|
||||
def _get_job_queue(self, raise_exception: bool = True) -> Optional["JobQueue"]:
|
||||
if self.__job_queue is None:
|
||||
if raise_exception:
|
||||
raise RuntimeError("JobQueue not set")
|
||||
return None
|
||||
job_queue = self.__job_queue()
|
||||
if job_queue is None:
|
||||
if raise_exception:
|
||||
raise RuntimeError("JobQueue was garbage collected")
|
||||
return None
|
||||
return job_queue
|
||||
|
||||
def schedule_timeout(
|
||||
self,
|
||||
callback: JobCallback,
|
||||
when: Union[float, dtm.timedelta, dtm.datetime, dtm.time],
|
||||
cancel_keys: Optional[Sequence[_KT]] = None,
|
||||
job_kwargs: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Schedule a timeout job. This is a thin wrapper around JobQueue.run_once.
|
||||
The callback will have to take care of resetting any state if necessary.
|
||||
Pass cancel_keys to automatically cancel the job when a new state is set for any of the
|
||||
keys.
|
||||
"""
|
||||
job_kwargs = job_kwargs or {}
|
||||
if cancel_keys:
|
||||
if "name" in job_kwargs:
|
||||
raise ValueError("job_kwargs must not contain a 'name' key")
|
||||
job_kwargs["name"] = self._build_job_name(cancel_keys)
|
||||
self._get_job_queue().run_once(callback, when, **job_kwargs)
|
||||
_LOGGER.debug(
|
||||
"Scheduled timeout. Will be cancelled when a new set state is for either of: %s",
|
||||
cancel_keys or [],
|
||||
)
|
||||
|
||||
|
||||
class SingleStateMachine(FiniteStateMachine[None]):
|
||||
def get_state_info(self, update: object) -> StateInfo[None]: # noqa: ARG002
|
||||
return StateInfo(None, State.IDLE, 0)
|
||||
|
||||
def do_set_state(self, key: None, state: State) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,114 @@
|
||||
"""This Module contains implementations of State classes for Finite State Machines"""
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
from typing import ClassVar, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class State(abc.ABC):
|
||||
__knows_uids: ClassVar[set[str]] = set()
|
||||
__not_cache: ClassVar[dict[str, "_NOTState"]] = {}
|
||||
__or_cache: ClassVar[dict[tuple[str, str], "_ORState"]] = {}
|
||||
__and_cache: ClassVar[dict[tuple[str, str], "_ANDState"]] = {}
|
||||
__xor_cache: ClassVar[dict[tuple[str, str], "_XORState"]] = {}
|
||||
|
||||
IDLE: "State"
|
||||
"""Default State for all Finite State Machines"""
|
||||
ANY: "State"
|
||||
"""Special State that matches any other State. Useful to define fallback behavior.
|
||||
*Not* supported in ``set_state`` method of FSMs.
|
||||
"""
|
||||
|
||||
def __init__(self, uid: Optional[str] = None):
|
||||
effective_uid = uid or uuid4().hex
|
||||
if effective_uid in self.__knows_uids:
|
||||
raise ValueError(f"Duplicate UID: {effective_uid} already registered")
|
||||
self._uid = effective_uid
|
||||
self.__knows_uids.add(effective_uid)
|
||||
|
||||
def __invert__(self) -> "_NOTState":
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__not_cache[self.uid]
|
||||
return self.__not_cache.setdefault(self.uid, _NOTState(self))
|
||||
|
||||
def __or__(self, other: "State") -> "_ORState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__or_cache[key]
|
||||
return self.__or_cache.setdefault(key, _ORState(self, other))
|
||||
|
||||
def __and__(self, other: "State") -> "_ANDState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__and_cache[key]
|
||||
return self.__and_cache.setdefault(key, _ANDState(self, other))
|
||||
|
||||
def __xor__(self, other: "State") -> "_XORState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__xor_cache[key]
|
||||
return self.__xor_cache.setdefault(key, _XORState(self, other))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}: {self.uid}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.uid
|
||||
|
||||
@property
|
||||
def uid(self) -> str:
|
||||
return self._uid
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
if isinstance(state, (_NOTState, _ANDState, _ORState, _XORState)):
|
||||
return state.matches(self)
|
||||
return self.uid == state.uid
|
||||
|
||||
|
||||
class _AnyState(State):
|
||||
def matches(self, state: "State") -> bool: # noqa: ARG002
|
||||
return True
|
||||
|
||||
|
||||
State.IDLE = State("IDLE")
|
||||
State.ANY = _AnyState("ANY")
|
||||
|
||||
|
||||
class _XORState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})^({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) ^ self._state_two.matches(state)
|
||||
|
||||
|
||||
class _ORState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})|({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) or self._state_two.matches(state)
|
||||
|
||||
|
||||
class _ANDState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})&({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) and self._state_two.matches(state)
|
||||
|
||||
|
||||
class _NOTState(State):
|
||||
def __init__(self, state: State):
|
||||
super().__init__(uid=f"!({state.uid})")
|
||||
self._state = state
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return not self._state.matches(state)
|
||||
@@ -97,7 +97,7 @@ class JobQueue(Generic[CCT]):
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_application", "_executor", "scheduler")
|
||||
__slots__ = ("__weakref__", "_application", "_executor", "scheduler")
|
||||
_CRON_MAPPING = ("sun", "mon", "tue", "wed", "thu", "fri", "sat")
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
+110
-29
@@ -30,8 +30,7 @@ from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DEFAULT_NONE, D
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import DVType, ODVInput
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext._utils.networkloop import network_retry_loop
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
try:
|
||||
from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer
|
||||
@@ -207,7 +206,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
self,
|
||||
poll_interval: float = 0.0,
|
||||
timeout: int = 10,
|
||||
bootstrap_retries: int = 0,
|
||||
bootstrap_retries: int = -1,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -226,16 +225,12 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
Telegram in seconds. Default is ``0.0``.
|
||||
timeout (:obj:`int`, optional): Passed to
|
||||
:paramref:`telegram.Bot.get_updates.timeout`. Defaults to ``10`` seconds.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
* < 0 - retry indefinitely (default)
|
||||
* 0 - no retries
|
||||
* > 0 - retry up to X times
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
The default value will be changed to from ``-1`` to ``0``. Indefinite retries
|
||||
during bootstrapping are not recommended.
|
||||
read_timeout (:obj:`float`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
@@ -414,14 +409,12 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
# updates from Telegram and inserts them in the update queue of the
|
||||
# Application.
|
||||
self.__polling_task = asyncio.create_task(
|
||||
network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
self._network_loop_retry(
|
||||
action_cb=polling_action_cb,
|
||||
on_err_cb=error_callback or default_error_callback,
|
||||
description="Polling Updates",
|
||||
description="getting Updates",
|
||||
interval=poll_interval,
|
||||
stop_event=self.__polling_task_stop_event,
|
||||
max_retries=-1,
|
||||
),
|
||||
name="Updater:start_polling:polling_task",
|
||||
)
|
||||
@@ -514,8 +507,8 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
Telegram servers before actually starting to poll. Default is :obj:`False`.
|
||||
|
||||
.. versionadded :: 13.4
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
@@ -705,6 +698,78 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
# say differently!
|
||||
return f"{protocol}://{listen}:{port}{url_path}"
|
||||
|
||||
async def _network_loop_retry(
|
||||
self,
|
||||
action_cb: Callable[..., Coroutine],
|
||||
on_err_cb: Callable[[TelegramError], None],
|
||||
description: str,
|
||||
interval: float,
|
||||
stop_event: Optional[asyncio.Event],
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
|
||||
`action_cb` evaluates :obj:`False`.
|
||||
|
||||
Args:
|
||||
action_cb (:term:`coroutine function`): Network oriented callback function to call.
|
||||
on_err_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives
|
||||
the exception object as a parameter.
|
||||
description (:obj:`str`): Description text to use for logs and exception raised.
|
||||
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
|
||||
`action_cb`.
|
||||
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
|
||||
loop. Setting the event will make the loop exit even if `action_cb` is currently
|
||||
running.
|
||||
|
||||
"""
|
||||
|
||||
async def do_action() -> bool:
|
||||
if not stop_event:
|
||||
return await action_cb()
|
||||
|
||||
action_cb_task = asyncio.create_task(action_cb())
|
||||
stop_task = asyncio.create_task(stop_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if stop_task in done:
|
||||
_LOGGER.debug("Network loop retry %s was cancelled", description)
|
||||
return False
|
||||
|
||||
return action_cb_task.result()
|
||||
|
||||
_LOGGER.debug("Start network loop retry %s", description)
|
||||
cur_interval = interval
|
||||
while self.running:
|
||||
try:
|
||||
if not await do_action():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
_LOGGER.info("%s", exc)
|
||||
cur_interval = 0.5 + exc.retry_after
|
||||
except TimedOut as toe:
|
||||
_LOGGER.debug("Timed out %s: %s", description, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken:
|
||||
_LOGGER.exception("Invalid token; aborting")
|
||||
raise
|
||||
except TelegramError as telegram_exc:
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
# increase waiting times on subsequent errors up to 30secs
|
||||
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
|
||||
else:
|
||||
cur_interval = interval
|
||||
|
||||
if cur_interval:
|
||||
await asyncio.sleep(cur_interval)
|
||||
|
||||
async def _bootstrap(
|
||||
self,
|
||||
max_retries: int,
|
||||
@@ -721,6 +786,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
updates if appropriate. If there are unsuccessful attempts, this will retry as specified by
|
||||
:paramref:`max_retries`.
|
||||
"""
|
||||
retries = 0
|
||||
|
||||
async def bootstrap_del_webhook() -> bool:
|
||||
_LOGGER.debug("Deleting webhook")
|
||||
@@ -744,30 +810,45 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
)
|
||||
return False
|
||||
|
||||
def bootstrap_on_err_cb(exc: Exception) -> None:
|
||||
# We need this since retries is an immutable object otherwise and the changes
|
||||
# wouldn't propagate outside of thi function
|
||||
nonlocal retries
|
||||
|
||||
if not isinstance(exc, InvalidToken) and (max_retries < 0 or retries < max_retries):
|
||||
retries += 1
|
||||
_LOGGER.warning(
|
||||
"Failed bootstrap phase; try=%s max_retries=%s", retries, max_retries
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Failed bootstrap phase after %s retries (%s)", retries, exc)
|
||||
raise exc
|
||||
|
||||
# Dropping pending updates from TG can be efficiently done with the drop_pending_updates
|
||||
# parameter of delete/start_webhook, even in the case of polling. Also, we want to make
|
||||
# sure that no webhook is configured in case of polling, so we just always call
|
||||
# delete_webhook for polling
|
||||
if drop_pending_updates or not webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_del_webhook,
|
||||
description="Bootstrap delete Webhook",
|
||||
interval=bootstrap_interval,
|
||||
await self._network_loop_retry(
|
||||
bootstrap_del_webhook,
|
||||
bootstrap_on_err_cb,
|
||||
"bootstrap del webhook",
|
||||
bootstrap_interval,
|
||||
stop_event=None,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
# Reset the retries counter for the next _network_loop_retry call
|
||||
retries = 0
|
||||
|
||||
# Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
|
||||
# so we set it anyhow.
|
||||
if webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_set_webhook,
|
||||
description="Bootstrap Set Webhook",
|
||||
interval=bootstrap_interval,
|
||||
await self._network_loop_retry(
|
||||
bootstrap_set_webhook,
|
||||
bootstrap_on_err_cb,
|
||||
"bootstrap set webhook",
|
||||
bootstrap_interval,
|
||||
stop_event=None,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains helper functions related to the std-lib asyncio module.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
.. versionadded:: NEXT.VERSION
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains a network retry loop implementation.
|
||||
Its specifically tailored to handling the Telegram API and its errors.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Hint:
|
||||
It was originally part of the `Updater` class, but as part of #4657 it was extracted into its
|
||||
own module to be used by other parts of the library.
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Coroutine
|
||||
from typing import Callable, Optional
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
async def network_retry_loop(
|
||||
*,
|
||||
action_cb: Callable[..., Coroutine],
|
||||
on_err_cb: Optional[Callable[[TelegramError], None]] = None,
|
||||
description: str,
|
||||
interval: float,
|
||||
stop_event: Optional[asyncio.Event] = None,
|
||||
is_running: Optional[Callable[[], bool]] = None,
|
||||
max_retries: int,
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop:
|
||||
* `is_running()` evaluates :obj:`False` or
|
||||
* return value of `action_cb` evaluates :obj:`False`
|
||||
* or `stop_event` is set.
|
||||
* or `max_retries` is reached.
|
||||
|
||||
Args:
|
||||
action_cb (:term:`coroutine function`): Network oriented callback function to call.
|
||||
on_err_cb (:obj:`callable`): Optional. Callback to call when TelegramError is caught.
|
||||
Receives the exception object as a parameter.
|
||||
|
||||
Hint:
|
||||
Only required if you want to handle the error in a special way. Logging about
|
||||
the error is already handled by the loop.
|
||||
|
||||
Important:
|
||||
Must not raise exceptions! If it does, the loop will be aborted.
|
||||
description (:obj:`str`): Description text to use for logs and exception raised.
|
||||
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
|
||||
`action_cb`.
|
||||
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
|
||||
loop. Setting the event will make the loop exit even if `action_cb` is currently
|
||||
running. Defaults to :obj:`None`.
|
||||
is_running (:obj:`callable`): Function to check if the loop should continue running.
|
||||
Must return a boolean value. Defaults to `lambda: True`.
|
||||
max_retries (:obj:`int`): Maximum number of retries before stopping the loop.
|
||||
|
||||
* < 0: Retry indefinitely.
|
||||
* 0: No retries.
|
||||
* > 0: Number of retries.
|
||||
|
||||
"""
|
||||
log_prefix = f"Network Retry Loop ({description}):"
|
||||
effective_is_running = is_running or (lambda: True)
|
||||
|
||||
async def do_action() -> bool:
|
||||
if not stop_event:
|
||||
return await action_cb()
|
||||
|
||||
action_cb_task = asyncio.create_task(action_cb())
|
||||
stop_task = asyncio.create_task(stop_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if stop_task in done:
|
||||
_LOGGER.debug("%s Cancelled", log_prefix)
|
||||
return False
|
||||
|
||||
return action_cb_task.result()
|
||||
|
||||
_LOGGER.debug("%s Starting", log_prefix)
|
||||
cur_interval = interval
|
||||
retries = 0
|
||||
while effective_is_running():
|
||||
try:
|
||||
if not await do_action():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
slack_time = 0.5
|
||||
_LOGGER.info(
|
||||
"%s %s. Adding %s seconds to the specified time.", log_prefix, exc, slack_time
|
||||
)
|
||||
cur_interval = slack_time + exc.retry_after
|
||||
except TimedOut as toe:
|
||||
_LOGGER.debug("%s Timed out: %s. Retrying immediately.", log_prefix, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken:
|
||||
_LOGGER.exception("%s Invalid token. Aborting retry loop.", log_prefix)
|
||||
raise
|
||||
except TelegramError as telegram_exc:
|
||||
if on_err_cb:
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
if max_retries < 0 or retries < max_retries:
|
||||
_LOGGER.debug(
|
||||
"%s Failed run number %s of %s. Retrying.", log_prefix, retries, max_retries
|
||||
)
|
||||
else:
|
||||
_LOGGER.exception(
|
||||
"%s Failed run number %s of %s. Aborting.", log_prefix, retries, max_retries
|
||||
)
|
||||
raise
|
||||
|
||||
# increase waiting times on subsequent errors up to 30secs
|
||||
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
|
||||
else:
|
||||
cur_interval = interval
|
||||
finally:
|
||||
retries += 1
|
||||
|
||||
if cur_interval:
|
||||
await asyncio.sleep(cur_interval)
|
||||
@@ -58,8 +58,6 @@ def input_media_video(class_thumb_file):
|
||||
parse_mode=InputMediaVideoTestBase.parse_mode,
|
||||
caption_entities=InputMediaVideoTestBase.caption_entities,
|
||||
thumbnail=class_thumb_file,
|
||||
cover=class_thumb_file,
|
||||
start_timestamp=InputMediaVideoTestBase.start_timestamp,
|
||||
supports_streaming=InputMediaVideoTestBase.supports_streaming,
|
||||
has_spoiler=InputMediaVideoTestBase.has_spoiler,
|
||||
show_caption_above_media=InputMediaVideoTestBase.show_caption_above_media,
|
||||
@@ -132,8 +130,6 @@ def input_paid_media_video(class_thumb_file):
|
||||
return InputPaidMediaVideo(
|
||||
media=InputMediaVideoTestBase.media,
|
||||
thumbnail=class_thumb_file,
|
||||
cover=class_thumb_file,
|
||||
start_timestamp=InputMediaVideoTestBase.start_timestamp,
|
||||
width=InputMediaVideoTestBase.width,
|
||||
height=InputMediaVideoTestBase.height,
|
||||
duration=InputMediaVideoTestBase.duration,
|
||||
@@ -148,7 +144,6 @@ class InputMediaVideoTestBase:
|
||||
width = 3
|
||||
height = 4
|
||||
duration = 5
|
||||
start_timestamp = 3
|
||||
parse_mode = "HTML"
|
||||
supports_streaming = True
|
||||
caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)]
|
||||
@@ -174,8 +169,6 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
assert input_media_video.caption_entities == tuple(self.caption_entities)
|
||||
assert input_media_video.supports_streaming == self.supports_streaming
|
||||
assert isinstance(input_media_video.thumbnail, InputFile)
|
||||
assert isinstance(input_media_video.cover, InputFile)
|
||||
assert input_media_video.start_timestamp == self.start_timestamp
|
||||
assert input_media_video.has_spoiler == self.has_spoiler
|
||||
assert input_media_video.show_caption_above_media == self.show_caption_above_media
|
||||
|
||||
@@ -201,8 +194,6 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
input_media_video_dict["show_caption_above_media"]
|
||||
== input_media_video.show_caption_above_media
|
||||
)
|
||||
assert input_media_video_dict["cover"] == input_media_video.cover
|
||||
assert input_media_video_dict["start_timestamp"] == input_media_video.start_timestamp
|
||||
|
||||
def test_with_video(self, video):
|
||||
# fixture found in test_video
|
||||
@@ -223,13 +214,10 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
|
||||
def test_with_local_files(self):
|
||||
input_media_video = InputMediaVideo(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
cover=data_file("telegram.jpg"),
|
||||
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
|
||||
)
|
||||
assert input_media_video.media == data_file("telegram.mp4").as_uri()
|
||||
assert input_media_video.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
assert input_media_video.cover == data_file("telegram.jpg").as_uri()
|
||||
|
||||
def test_type_enum_conversion(self):
|
||||
# Since we have a lot of different test classes for all the input media types, we test this
|
||||
@@ -577,8 +565,6 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
assert input_paid_media_video.duration == self.duration
|
||||
assert input_paid_media_video.supports_streaming == self.supports_streaming
|
||||
assert isinstance(input_paid_media_video.thumbnail, InputFile)
|
||||
assert isinstance(input_paid_media_video.cover, InputFile)
|
||||
assert input_paid_media_video.start_timestamp == self.start_timestamp
|
||||
|
||||
def test_to_dict(self, input_paid_media_video):
|
||||
input_paid_media_video_dict = input_paid_media_video.to_dict()
|
||||
@@ -592,11 +578,6 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
== input_paid_media_video.supports_streaming
|
||||
)
|
||||
assert input_paid_media_video_dict["thumbnail"] == input_paid_media_video.thumbnail
|
||||
assert input_paid_media_video_dict["cover"] == input_paid_media_video.cover
|
||||
assert (
|
||||
input_paid_media_video_dict["start_timestamp"]
|
||||
== input_paid_media_video.start_timestamp
|
||||
)
|
||||
|
||||
def test_with_video(self, video):
|
||||
# fixture found in test_video
|
||||
@@ -615,13 +596,10 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
|
||||
def test_with_local_files(self):
|
||||
input_paid_media_video = InputPaidMediaVideo(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
cover=data_file("telegram.jpg"),
|
||||
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
|
||||
)
|
||||
assert input_paid_media_video.media == data_file("telegram.mp4").as_uri()
|
||||
assert input_paid_media_video.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
assert input_paid_media_video.cover == data_file("telegram.jpg").as_uri()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -46,8 +46,6 @@ class VideoTestBase:
|
||||
mime_type = "video/mp4"
|
||||
supports_streaming = True
|
||||
file_name = "telegram.mp4"
|
||||
start_timestamp = 3
|
||||
cover = (PhotoSize("file_id", "unique_id", 640, 360, file_size=0),)
|
||||
thumb_width = 180
|
||||
thumb_height = 320
|
||||
thumb_file_size = 1767
|
||||
@@ -94,8 +92,6 @@ class TestVideoWithoutRequest(VideoTestBase):
|
||||
"mime_type": self.mime_type,
|
||||
"file_size": self.file_size,
|
||||
"file_name": self.file_name,
|
||||
"start_timestamp": self.start_timestamp,
|
||||
"cover": [photo_size.to_dict() for photo_size in self.cover],
|
||||
}
|
||||
json_video = Video.de_json(json_dict, offline_bot)
|
||||
assert json_video.api_kwargs == {}
|
||||
@@ -108,8 +104,6 @@ class TestVideoWithoutRequest(VideoTestBase):
|
||||
assert json_video.mime_type == self.mime_type
|
||||
assert json_video.file_size == self.file_size
|
||||
assert json_video.file_name == self.file_name
|
||||
assert json_video.start_timestamp == self.start_timestamp
|
||||
assert json_video.cover == self.cover
|
||||
|
||||
def test_to_dict(self, video):
|
||||
video_dict = video.to_dict()
|
||||
@@ -229,9 +223,7 @@ class TestVideoWithoutRequest(VideoTestBase):
|
||||
|
||||
class TestVideoWithRequest(VideoTestBase):
|
||||
@pytest.mark.parametrize("duration", [dtm.timedelta(seconds=5), 5])
|
||||
async def test_send_all_args(
|
||||
self, bot, chat_id, video_file, video, thumb_file, photo_file, duration
|
||||
):
|
||||
async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file, duration):
|
||||
message = await bot.send_video(
|
||||
chat_id,
|
||||
video_file,
|
||||
@@ -244,8 +236,6 @@ class TestVideoWithRequest(VideoTestBase):
|
||||
height=video.height,
|
||||
parse_mode="Markdown",
|
||||
thumbnail=thumb_file,
|
||||
cover=photo_file,
|
||||
start_timestamp=self.start_timestamp,
|
||||
has_spoiler=True,
|
||||
show_caption_above_media=True,
|
||||
)
|
||||
@@ -266,11 +256,6 @@ class TestVideoWithRequest(VideoTestBase):
|
||||
assert message.video.thumbnail.width == self.thumb_width
|
||||
assert message.video.thumbnail.height == self.thumb_height
|
||||
|
||||
assert message.video.start_timestamp == self.start_timestamp
|
||||
|
||||
assert isinstance(message.video.cover, tuple)
|
||||
assert isinstance(message.video.cover[0], PhotoSize)
|
||||
|
||||
assert message.video.file_name == self.file_name
|
||||
assert message.has_protected_content
|
||||
assert message.has_media_spoiler
|
||||
|
||||
@@ -28,6 +28,7 @@ from telegram import (
|
||||
InputTextMessageContent,
|
||||
)
|
||||
from telegram.constants import InlineQueryResultType
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@@ -39,6 +40,7 @@ def inline_query_result_article():
|
||||
input_message_content=InlineQueryResultArticleTestBase.input_message_content,
|
||||
reply_markup=InlineQueryResultArticleTestBase.reply_markup,
|
||||
url=InlineQueryResultArticleTestBase.url,
|
||||
hide_url=InlineQueryResultArticleTestBase.hide_url,
|
||||
description=InlineQueryResultArticleTestBase.description,
|
||||
thumbnail_url=InlineQueryResultArticleTestBase.thumbnail_url,
|
||||
thumbnail_height=InlineQueryResultArticleTestBase.thumbnail_height,
|
||||
@@ -53,6 +55,7 @@ class InlineQueryResultArticleTestBase:
|
||||
input_message_content = InputTextMessageContent("input_message_content")
|
||||
reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]])
|
||||
url = "url"
|
||||
hide_url = True
|
||||
description = "description"
|
||||
thumbnail_url = "thumb url"
|
||||
thumbnail_height = 10
|
||||
@@ -76,6 +79,7 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
|
||||
)
|
||||
assert inline_query_result_article.reply_markup.to_dict() == self.reply_markup.to_dict()
|
||||
assert inline_query_result_article.url == self.url
|
||||
assert inline_query_result_article.hide_url == self.hide_url
|
||||
assert inline_query_result_article.description == self.description
|
||||
assert inline_query_result_article.thumbnail_url == self.thumbnail_url
|
||||
assert inline_query_result_article.thumbnail_height == self.thumbnail_height
|
||||
@@ -97,6 +101,7 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
|
||||
== inline_query_result_article.reply_markup.to_dict()
|
||||
)
|
||||
assert inline_query_result_article_dict["url"] == inline_query_result_article.url
|
||||
assert inline_query_result_article_dict["hide_url"] == inline_query_result_article.hide_url
|
||||
assert (
|
||||
inline_query_result_article_dict["description"]
|
||||
== inline_query_result_article.description
|
||||
@@ -153,3 +158,31 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
def test_deprecation_warning_for_hide_url(self):
|
||||
with pytest.warns(PTBDeprecationWarning, match="The argument `hide_url`") as record:
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=True
|
||||
)
|
||||
|
||||
assert record[0].filename == __file__, "wrong stacklevel!"
|
||||
|
||||
with pytest.warns(PTBDeprecationWarning, match="The argument `hide_url`") as record:
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=False
|
||||
)
|
||||
|
||||
assert record[0].filename == __file__, "wrong stacklevel!"
|
||||
|
||||
assert (
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=True
|
||||
).hide_url
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=False
|
||||
).hide_url
|
||||
is False
|
||||
)
|
||||
|
||||
@@ -282,10 +282,10 @@ class TestInputInvoiceMessageContentWithoutRequest(InputInvoiceMessageContentTes
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
self.provider_token,
|
||||
self.currency,
|
||||
# the first prices amount & the second lebal changed
|
||||
[LabeledPrice("label1", 24), LabeledPrice("label22", 314)],
|
||||
self.provider_token,
|
||||
)
|
||||
d = InputInvoiceMessageContent(
|
||||
self.title,
|
||||
|
||||
@@ -22,14 +22,12 @@ import pytest
|
||||
|
||||
from telegram import (
|
||||
AffiliateInfo,
|
||||
Chat,
|
||||
Gift,
|
||||
PaidMediaVideo,
|
||||
RevenueWithdrawalStatePending,
|
||||
Sticker,
|
||||
TransactionPartner,
|
||||
TransactionPartnerAffiliateProgram,
|
||||
TransactionPartnerChat,
|
||||
TransactionPartnerFragment,
|
||||
TransactionPartnerOther,
|
||||
TransactionPartnerTelegramAds,
|
||||
@@ -97,10 +95,6 @@ class TransactionPartnerTestBase:
|
||||
amount=42,
|
||||
)
|
||||
request_count = 42
|
||||
chat = Chat(
|
||||
id=3,
|
||||
type=Chat.CHANNEL,
|
||||
)
|
||||
|
||||
|
||||
class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
|
||||
@@ -129,7 +123,6 @@ class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
|
||||
("telegram_ads", TransactionPartnerTelegramAds),
|
||||
("telegram_api", TransactionPartnerTelegramApi),
|
||||
("other", TransactionPartnerOther),
|
||||
("chat", TransactionPartnerChat),
|
||||
],
|
||||
)
|
||||
def test_subclass(self, offline_bot, tp_type, subclass):
|
||||
@@ -457,58 +450,3 @@ class TestTransactionPartnerTelegramApiWithoutRequest(TransactionPartnerTestBase
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def transaction_partner_chat():
|
||||
return TransactionPartnerChat(
|
||||
chat=TransactionPartnerTestBase.chat,
|
||||
gift=TransactionPartnerTestBase.gift,
|
||||
)
|
||||
|
||||
|
||||
class TestTransactionPartnerChatWithoutRequest(TransactionPartnerTestBase):
|
||||
type = TransactionPartnerType.CHAT
|
||||
|
||||
def test_slot_behaviour(self, transaction_partner_chat):
|
||||
inst = transaction_partner_chat
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"chat": self.chat.to_dict(),
|
||||
"gift": self.gift.to_dict(),
|
||||
}
|
||||
tp = TransactionPartnerChat.de_json(json_dict, offline_bot)
|
||||
assert tp.api_kwargs == {}
|
||||
assert tp.type == "chat"
|
||||
assert tp.chat == self.chat
|
||||
assert tp.gift == self.gift
|
||||
|
||||
def test_to_dict(self, transaction_partner_chat):
|
||||
json_dict = transaction_partner_chat.to_dict()
|
||||
assert json_dict["type"] == self.type
|
||||
assert json_dict["chat"] == self.chat.to_dict()
|
||||
assert json_dict["gift"] == self.gift.to_dict()
|
||||
|
||||
def test_equality(self, transaction_partner_chat):
|
||||
a = transaction_partner_chat
|
||||
b = TransactionPartnerChat(
|
||||
chat=self.chat,
|
||||
gift=self.gift,
|
||||
)
|
||||
c = TransactionPartnerChat(
|
||||
chat=Chat(id=1, type=Chat.CHANNEL),
|
||||
)
|
||||
d = Chat(id=1, type=Chat.CHANNEL)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
@@ -269,9 +269,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token,
|
||||
**kwargs,
|
||||
)
|
||||
for kwargs in ({}, {"protect_content": False})
|
||||
@@ -301,6 +301,7 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
"", # using tg stars
|
||||
"XTR",
|
||||
[self.prices[0]],
|
||||
allow_sending_without_reply=custom,
|
||||
@@ -314,9 +315,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token,
|
||||
reply_to_message_id=reply_to_message.message_id,
|
||||
)
|
||||
assert message.reply_to_message is None
|
||||
@@ -327,9 +328,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token,
|
||||
reply_to_message_id=reply_to_message.message_id,
|
||||
)
|
||||
|
||||
@@ -339,9 +340,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token=provider_token,
|
||||
max_tip_amount=self.max_tip_amount,
|
||||
suggested_tip_amounts=self.suggested_tip_amounts,
|
||||
start_parameter=self.start_parameter,
|
||||
|
||||
@@ -351,9 +351,6 @@ def build_kwargs(
|
||||
allow_sending_without_reply=manually_passed_value,
|
||||
quote_parse_mode=manually_passed_value,
|
||||
)
|
||||
# TODO remove when gift_id isnt marked as optional anymore, tags: deprecated 21.11
|
||||
elif name == "gift_id":
|
||||
kws[name] = "GIFT-ID"
|
||||
|
||||
return kws
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ from typing import Optional
|
||||
import pytest
|
||||
|
||||
from telegram import Bot, Chat, Message, MessageEntity, User
|
||||
from telegram.error import InvalidToken, TelegramError
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
ApplicationBuilder,
|
||||
@@ -2359,47 +2359,6 @@ class TestApplication:
|
||||
for record in recwarn:
|
||||
assert not str(record.message).startswith("Could not add signal handlers for the stop")
|
||||
|
||||
@pytest.mark.parametrize("exception_class", [InvalidToken, TelegramError])
|
||||
@pytest.mark.parametrize("retries", [3, 0])
|
||||
@pytest.mark.parametrize("method_name", ["run_polling", "run_webhook"])
|
||||
async def test_run_polling_webhook_bootstrap_retries(
|
||||
self, monkeypatch, exception_class, retries, offline_bot, method_name
|
||||
):
|
||||
"""This doesn't test all of the internals of the network retry loop. We do that quite
|
||||
intensively for the `Updater` and here we just want to make sure that the `Application`
|
||||
does do the retries.
|
||||
"""
|
||||
|
||||
def thread_target():
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
app = (
|
||||
ApplicationBuilder().bot(offline_bot).application_class(PytestApplication).build()
|
||||
)
|
||||
|
||||
async def initialize(*args, **kwargs):
|
||||
self.count += 1
|
||||
raise exception_class(str(self.count))
|
||||
|
||||
monkeypatch.setattr(app, "initialize", initialize)
|
||||
method = functools.partial(
|
||||
getattr(app, method_name),
|
||||
bootstrap_retries=retries,
|
||||
close_loop=False,
|
||||
stop_signals=None,
|
||||
)
|
||||
|
||||
if exception_class == InvalidToken:
|
||||
with pytest.raises(InvalidToken, match="1"):
|
||||
method()
|
||||
else:
|
||||
with pytest.raises(TelegramError, match=str(retries + 1)):
|
||||
method()
|
||||
|
||||
thread = Thread(target=thread_target)
|
||||
thread.start()
|
||||
thread.join(timeout=10)
|
||||
assert not thread.is_alive(), "Test took to long to run. Aborting"
|
||||
|
||||
@pytest.mark.flaky(3, 1) # loop.call_later will error the test when a flood error is received
|
||||
def test_signal_handlers(self, app, monkeypatch):
|
||||
# this test should make sure that signal handlers are set by default on Linux + Mac,
|
||||
|
||||
@@ -1617,7 +1617,6 @@ class TestBotWithoutRequest:
|
||||
== [MessageEntity(MessageEntity.BOLD, 0, 4).to_dict()],
|
||||
data["protect_content"] is True,
|
||||
data["message_thread_id"] == 1,
|
||||
data["video_start_timestamp"] == 999,
|
||||
]
|
||||
):
|
||||
pytest.fail("I got wrong parameters in post")
|
||||
@@ -1629,7 +1628,6 @@ class TestBotWithoutRequest:
|
||||
from_chat_id=chat_id,
|
||||
message_id=media_message.message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=999,
|
||||
caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 4)],
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_to_message_id=media_message.message_id,
|
||||
|
||||
+149
-356
@@ -16,6 +16,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -34,232 +35,147 @@ from telegram.constants import BotCommandScopeType
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bot_command_scope():
|
||||
return BotCommandScope(BotCommandScopeTestBase.type)
|
||||
@pytest.fixture(scope="module", params=["str", "int"])
|
||||
def chat_id(request):
|
||||
if request.param == "str":
|
||||
return "@supergroupusername"
|
||||
return 43
|
||||
|
||||
|
||||
class BotCommandScopeTestBase:
|
||||
type = BotCommandScopeType.DEFAULT
|
||||
chat_id = 123456789
|
||||
user_id = 987654321
|
||||
@pytest.fixture(
|
||||
scope="class",
|
||||
params=[
|
||||
BotCommandScope.DEFAULT,
|
||||
BotCommandScope.ALL_PRIVATE_CHATS,
|
||||
BotCommandScope.ALL_GROUP_CHATS,
|
||||
BotCommandScope.ALL_CHAT_ADMINISTRATORS,
|
||||
BotCommandScope.CHAT,
|
||||
BotCommandScope.CHAT_ADMINISTRATORS,
|
||||
BotCommandScope.CHAT_MEMBER,
|
||||
],
|
||||
)
|
||||
def scope_type(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class TestBotCommandScopeWithoutRequest(BotCommandScopeTestBase):
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
BotCommandScopeDefault,
|
||||
BotCommandScopeAllPrivateChats,
|
||||
BotCommandScopeAllGroupChats,
|
||||
BotCommandScopeAllChatAdministrators,
|
||||
BotCommandScopeChat,
|
||||
BotCommandScopeChatAdministrators,
|
||||
BotCommandScopeChatMember,
|
||||
],
|
||||
ids=[
|
||||
BotCommandScope.DEFAULT,
|
||||
BotCommandScope.ALL_PRIVATE_CHATS,
|
||||
BotCommandScope.ALL_GROUP_CHATS,
|
||||
BotCommandScope.ALL_CHAT_ADMINISTRATORS,
|
||||
BotCommandScope.CHAT,
|
||||
BotCommandScope.CHAT_ADMINISTRATORS,
|
||||
BotCommandScope.CHAT_MEMBER,
|
||||
],
|
||||
)
|
||||
def scope_class(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
(BotCommandScopeDefault, BotCommandScope.DEFAULT),
|
||||
(BotCommandScopeAllPrivateChats, BotCommandScope.ALL_PRIVATE_CHATS),
|
||||
(BotCommandScopeAllGroupChats, BotCommandScope.ALL_GROUP_CHATS),
|
||||
(BotCommandScopeAllChatAdministrators, BotCommandScope.ALL_CHAT_ADMINISTRATORS),
|
||||
(BotCommandScopeChat, BotCommandScope.CHAT),
|
||||
(BotCommandScopeChatAdministrators, BotCommandScope.CHAT_ADMINISTRATORS),
|
||||
(BotCommandScopeChatMember, BotCommandScope.CHAT_MEMBER),
|
||||
],
|
||||
ids=[
|
||||
BotCommandScope.DEFAULT,
|
||||
BotCommandScope.ALL_PRIVATE_CHATS,
|
||||
BotCommandScope.ALL_GROUP_CHATS,
|
||||
BotCommandScope.ALL_CHAT_ADMINISTRATORS,
|
||||
BotCommandScope.CHAT,
|
||||
BotCommandScope.CHAT_ADMINISTRATORS,
|
||||
BotCommandScope.CHAT_MEMBER,
|
||||
],
|
||||
)
|
||||
def scope_class_and_type(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def bot_command_scope(scope_class_and_type, chat_id):
|
||||
# we use de_json here so that we don't have to worry about which class needs which arguments
|
||||
return scope_class_and_type[0].de_json(
|
||||
{"type": scope_class_and_type[1], "chat_id": chat_id, "user_id": 42}, bot=None
|
||||
)
|
||||
|
||||
|
||||
# All the scope types are very similar, so we test everything via parametrization
|
||||
class TestBotCommandScopeWithoutRequest:
|
||||
def test_slot_behaviour(self, bot_command_scope):
|
||||
inst = bot_command_scope
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
for attr in bot_command_scope.__slots__:
|
||||
assert getattr(bot_command_scope, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(bot_command_scope)) == len(
|
||||
set(mro_slots(bot_command_scope))
|
||||
), "duplicate slot"
|
||||
|
||||
def test_type_enum_conversion(self, bot_command_scope):
|
||||
def test_de_json(self, offline_bot, scope_class_and_type, chat_id):
|
||||
cls = scope_class_and_type[0]
|
||||
type_ = scope_class_and_type[1]
|
||||
|
||||
json_dict = {"type": type_, "chat_id": chat_id, "user_id": 42}
|
||||
bot_command_scope = BotCommandScope.de_json(json_dict, offline_bot)
|
||||
assert set(bot_command_scope.api_kwargs.keys()) == {"chat_id", "user_id"} - set(
|
||||
cls.__slots__
|
||||
)
|
||||
|
||||
assert isinstance(bot_command_scope, BotCommandScope)
|
||||
assert isinstance(bot_command_scope, cls)
|
||||
assert bot_command_scope.type == type_
|
||||
if "chat_id" in cls.__slots__:
|
||||
assert bot_command_scope.chat_id == chat_id
|
||||
if "user_id" in cls.__slots__:
|
||||
assert bot_command_scope.user_id == 42
|
||||
|
||||
def test_de_json_invalid_type(self, offline_bot):
|
||||
json_dict = {"type": "invalid", "chat_id": chat_id, "user_id": 42}
|
||||
bot_command_scope = BotCommandScope.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(bot_command_scope) is BotCommandScope
|
||||
assert bot_command_scope.type == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, scope_class, offline_bot, chat_id):
|
||||
"""This makes sure that e.g. BotCommandScopeDefault(data) never returns a
|
||||
BotCommandScopeChat instance."""
|
||||
json_dict = {"type": "invalid", "chat_id": chat_id, "user_id": 42}
|
||||
assert type(scope_class.de_json(json_dict, offline_bot)) is scope_class
|
||||
|
||||
def test_to_dict(self, bot_command_scope):
|
||||
bot_command_scope_dict = bot_command_scope.to_dict()
|
||||
|
||||
assert isinstance(bot_command_scope_dict, dict)
|
||||
assert bot_command_scope["type"] == bot_command_scope.type
|
||||
if hasattr(bot_command_scope, "chat_id"):
|
||||
assert bot_command_scope["chat_id"] == bot_command_scope.chat_id
|
||||
if hasattr(bot_command_scope, "user_id"):
|
||||
assert bot_command_scope["user_id"] == bot_command_scope.user_id
|
||||
|
||||
def test_type_enum_conversion(self):
|
||||
assert type(BotCommandScope("default").type) is BotCommandScopeType
|
||||
assert BotCommandScope("unknown").type == "unknown"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"type": "unknown"}
|
||||
transaction_partner = BotCommandScope.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "unknown"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bcs_type", "subclass"),
|
||||
[
|
||||
("all_private_chats", BotCommandScopeAllPrivateChats),
|
||||
("all_chat_administrators", BotCommandScopeAllChatAdministrators),
|
||||
("all_group_chats", BotCommandScopeAllGroupChats),
|
||||
("chat", BotCommandScopeChat),
|
||||
("chat_administrators", BotCommandScopeChatAdministrators),
|
||||
("chat_member", BotCommandScopeChatMember),
|
||||
("default", BotCommandScopeDefault),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, bcs_type, subclass):
|
||||
json_dict = {
|
||||
"type": bcs_type,
|
||||
"chat_id": self.chat_id,
|
||||
"user_id": self.user_id,
|
||||
}
|
||||
bcs = BotCommandScope.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(bcs) is subclass
|
||||
assert set(bcs.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
|
||||
"type"
|
||||
}
|
||||
assert bcs.type == bcs_type
|
||||
|
||||
def test_to_dict(self, bot_command_scope):
|
||||
data = bot_command_scope.to_dict()
|
||||
assert data == {"type": "default"}
|
||||
|
||||
def test_equality(self, bot_command_scope):
|
||||
a = bot_command_scope
|
||||
b = BotCommandScope(self.type)
|
||||
c = BotCommandScope("unknown")
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def bot_command_scope_all_private_chats():
|
||||
return BotCommandScopeAllPrivateChats()
|
||||
|
||||
|
||||
class TestBotCommandScopeAllPrivateChatsWithoutRequest(BotCommandScopeTestBase):
|
||||
type = BotCommandScopeType.ALL_PRIVATE_CHATS
|
||||
|
||||
def test_slot_behaviour(self, bot_command_scope_all_private_chats):
|
||||
inst = bot_command_scope_all_private_chats
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = BotCommandScopeAllPrivateChats.de_json({}, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "all_private_chats"
|
||||
|
||||
def test_to_dict(self, bot_command_scope_all_private_chats):
|
||||
assert bot_command_scope_all_private_chats.to_dict() == {
|
||||
"type": bot_command_scope_all_private_chats.type
|
||||
}
|
||||
|
||||
def test_equality(self, bot_command_scope_all_private_chats):
|
||||
a = bot_command_scope_all_private_chats
|
||||
b = BotCommandScopeAllPrivateChats()
|
||||
c = Dice(5, "test")
|
||||
d = BotCommandScopeDefault()
|
||||
|
||||
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
|
||||
def bot_command_scope_all_chat_administrators():
|
||||
return BotCommandScopeAllChatAdministrators()
|
||||
|
||||
|
||||
class TestBotCommandScopeAllChatAdministratorsWithoutRequest(BotCommandScopeTestBase):
|
||||
type = BotCommandScopeType.ALL_CHAT_ADMINISTRATORS
|
||||
|
||||
def test_slot_behaviour(self, bot_command_scope_all_chat_administrators):
|
||||
inst = bot_command_scope_all_chat_administrators
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = BotCommandScopeAllChatAdministrators.de_json({}, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "all_chat_administrators"
|
||||
|
||||
def test_to_dict(self, bot_command_scope_all_chat_administrators):
|
||||
assert bot_command_scope_all_chat_administrators.to_dict() == {
|
||||
"type": bot_command_scope_all_chat_administrators.type
|
||||
}
|
||||
|
||||
def test_equality(self, bot_command_scope_all_chat_administrators):
|
||||
a = bot_command_scope_all_chat_administrators
|
||||
b = BotCommandScopeAllChatAdministrators()
|
||||
c = Dice(5, "test")
|
||||
d = BotCommandScopeDefault()
|
||||
|
||||
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
|
||||
def bot_command_scope_all_group_chats():
|
||||
return BotCommandScopeAllGroupChats()
|
||||
|
||||
|
||||
class TestBotCommandScopeAllGroupChatsWithoutRequest(BotCommandScopeTestBase):
|
||||
type = BotCommandScopeType.ALL_GROUP_CHATS
|
||||
|
||||
def test_slot_behaviour(self, bot_command_scope_all_group_chats):
|
||||
inst = bot_command_scope_all_group_chats
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = BotCommandScopeAllGroupChats.de_json({}, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "all_group_chats"
|
||||
|
||||
def test_to_dict(self, bot_command_scope_all_group_chats):
|
||||
assert bot_command_scope_all_group_chats.to_dict() == {
|
||||
"type": bot_command_scope_all_group_chats.type
|
||||
}
|
||||
|
||||
def test_equality(self, bot_command_scope_all_group_chats):
|
||||
a = bot_command_scope_all_group_chats
|
||||
b = BotCommandScopeAllGroupChats()
|
||||
c = Dice(5, "test")
|
||||
d = BotCommandScopeDefault()
|
||||
|
||||
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
|
||||
def bot_command_scope_chat():
|
||||
return BotCommandScopeChat(TestBotCommandScopeChatWithoutRequest.chat_id)
|
||||
|
||||
|
||||
class TestBotCommandScopeChatWithoutRequest(BotCommandScopeTestBase):
|
||||
type = BotCommandScopeType.CHAT
|
||||
|
||||
def test_slot_behaviour(self, bot_command_scope_chat):
|
||||
inst = bot_command_scope_chat
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = BotCommandScopeChat.de_json({"chat_id": self.chat_id}, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "chat"
|
||||
assert transaction_partner.chat_id == self.chat_id
|
||||
|
||||
def test_to_dict(self, bot_command_scope_chat):
|
||||
assert bot_command_scope_chat.to_dict() == {
|
||||
"type": bot_command_scope_chat.type,
|
||||
"chat_id": self.chat_id,
|
||||
}
|
||||
|
||||
def test_equality(self, bot_command_scope_chat):
|
||||
a = bot_command_scope_chat
|
||||
b = BotCommandScopeChat(self.chat_id)
|
||||
c = BotCommandScopeChat(self.chat_id + 1)
|
||||
d = Dice(5, "test")
|
||||
e = BotCommandScopeDefault()
|
||||
def test_equality(self, bot_command_scope, offline_bot):
|
||||
a = BotCommandScope("base_type")
|
||||
b = BotCommandScope("base_type")
|
||||
c = bot_command_scope
|
||||
d = deepcopy(bot_command_scope)
|
||||
e = Dice(4, "emoji")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -273,147 +189,24 @@ class TestBotCommandScopeChatWithoutRequest(BotCommandScopeTestBase):
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
@pytest.fixture
|
||||
def bot_command_scope_chat_administrators():
|
||||
return BotCommandScopeChatAdministrators(
|
||||
TestBotCommandScopeChatAdministratorsWithoutRequest.chat_id
|
||||
)
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
if hasattr(c, "chat_id"):
|
||||
json_dict = c.to_dict()
|
||||
json_dict["chat_id"] = 0
|
||||
f = c.__class__.de_json(json_dict, offline_bot)
|
||||
|
||||
class TestBotCommandScopeChatAdministratorsWithoutRequest(BotCommandScopeTestBase):
|
||||
type = BotCommandScopeType.CHAT_ADMINISTRATORS
|
||||
assert c != f
|
||||
assert hash(c) != hash(f)
|
||||
|
||||
def test_slot_behaviour(self, bot_command_scope_chat_administrators):
|
||||
inst = bot_command_scope_chat_administrators
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
if hasattr(c, "user_id"):
|
||||
json_dict = c.to_dict()
|
||||
json_dict["user_id"] = 0
|
||||
g = c.__class__.de_json(json_dict, offline_bot)
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = BotCommandScopeChatAdministrators.de_json(
|
||||
{"chat_id": self.chat_id}, offline_bot
|
||||
)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "chat_administrators"
|
||||
assert transaction_partner.chat_id == self.chat_id
|
||||
|
||||
def test_to_dict(self, bot_command_scope_chat_administrators):
|
||||
assert bot_command_scope_chat_administrators.to_dict() == {
|
||||
"type": bot_command_scope_chat_administrators.type,
|
||||
"chat_id": self.chat_id,
|
||||
}
|
||||
|
||||
def test_equality(self, bot_command_scope_chat_administrators):
|
||||
a = bot_command_scope_chat_administrators
|
||||
b = BotCommandScopeChatAdministrators(self.chat_id)
|
||||
c = BotCommandScopeChatAdministrators(self.chat_id + 1)
|
||||
d = Dice(5, "test")
|
||||
e = BotCommandScopeDefault()
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bot_command_scope_chat_member():
|
||||
return BotCommandScopeChatMember(
|
||||
TestBotCommandScopeChatMemberWithoutRequest.chat_id,
|
||||
TestBotCommandScopeChatMemberWithoutRequest.user_id,
|
||||
)
|
||||
|
||||
|
||||
class TestBotCommandScopeChatMemberWithoutRequest(BotCommandScopeTestBase):
|
||||
type = BotCommandScopeType.CHAT_MEMBER
|
||||
|
||||
def test_slot_behaviour(self, bot_command_scope_chat_member):
|
||||
inst = bot_command_scope_chat_member
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = BotCommandScopeChatMember.de_json(
|
||||
{"chat_id": self.chat_id, "user_id": self.user_id}, offline_bot
|
||||
)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "chat_member"
|
||||
assert transaction_partner.chat_id == self.chat_id
|
||||
assert transaction_partner.user_id == self.user_id
|
||||
|
||||
def test_to_dict(self, bot_command_scope_chat_member):
|
||||
assert bot_command_scope_chat_member.to_dict() == {
|
||||
"type": bot_command_scope_chat_member.type,
|
||||
"chat_id": self.chat_id,
|
||||
"user_id": self.user_id,
|
||||
}
|
||||
|
||||
def test_equality(self, bot_command_scope_chat_member):
|
||||
a = bot_command_scope_chat_member
|
||||
b = BotCommandScopeChatMember(self.chat_id, self.user_id)
|
||||
c = BotCommandScopeChatMember(self.chat_id + 1, self.user_id)
|
||||
d = BotCommandScopeChatMember(self.chat_id, self.user_id + 1)
|
||||
e = Dice(5, "test")
|
||||
f = BotCommandScopeDefault()
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
assert a != f
|
||||
assert hash(a) != hash(f)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bot_command_scope_default():
|
||||
return BotCommandScopeDefault()
|
||||
|
||||
|
||||
class TestBotCommandScopeDefaultWithoutRequest(BotCommandScopeTestBase):
|
||||
type = BotCommandScopeType.DEFAULT
|
||||
|
||||
def test_slot_behaviour(self, bot_command_scope_default):
|
||||
inst = bot_command_scope_default
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = BotCommandScopeDefault.de_json({}, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "default"
|
||||
|
||||
def test_to_dict(self, bot_command_scope_default):
|
||||
assert bot_command_scope_default.to_dict() == {"type": bot_command_scope_default.type}
|
||||
|
||||
def test_equality(self, bot_command_scope_default):
|
||||
a = bot_command_scope_default
|
||||
b = BotCommandScopeDefault()
|
||||
c = Dice(5, "test")
|
||||
d = BotCommandScopeChatMember(123, 456)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
assert c != g
|
||||
assert hash(c) != hash(g)
|
||||
|
||||
+5
-33
@@ -610,9 +610,9 @@ class TestChatWithoutRequest(ChatTestBase):
|
||||
"title",
|
||||
"description",
|
||||
"payload",
|
||||
"provider_token",
|
||||
"currency",
|
||||
"prices",
|
||||
"provider_token",
|
||||
)
|
||||
|
||||
async def test_instance_method_send_location(self, monkeypatch, chat):
|
||||
@@ -1312,7 +1312,7 @@ class TestChatWithoutRequest(ChatTestBase):
|
||||
)
|
||||
|
||||
async def test_instance_method_send_gift(self, monkeypatch, chat):
|
||||
async def make_assertion_private(*_, **kwargs):
|
||||
async def make_assertion(*_, **kwargs):
|
||||
return (
|
||||
kwargs["user_id"] == chat.id
|
||||
and kwargs["gift_id"] == "gift_id"
|
||||
@@ -1321,39 +1321,11 @@ class TestChatWithoutRequest(ChatTestBase):
|
||||
and kwargs["text_entities"] == "text_entities"
|
||||
)
|
||||
|
||||
async def make_assertion_channel(*_, **kwargs):
|
||||
return (
|
||||
kwargs["chat_id"] == chat.id
|
||||
and kwargs["gift_id"] == "gift_id"
|
||||
and kwargs["text"] == "text"
|
||||
and kwargs["text_parse_mode"] == "text_parse_mode"
|
||||
and kwargs["text_entities"] == "text_entities"
|
||||
)
|
||||
|
||||
# TODO discuss if better way exists
|
||||
# tags: deprecated 21.11
|
||||
with pytest.raises(
|
||||
Exception,
|
||||
match="Default for argument gift_id does not match the default of the Bot method.",
|
||||
):
|
||||
assert check_shortcut_signature(
|
||||
Chat.send_gift, Bot.send_gift, ["user_id", "chat_id"], []
|
||||
)
|
||||
assert await check_shortcut_call(
|
||||
chat.send_gift, chat.get_bot(), "send_gift", ["user_id", "chat_id"]
|
||||
)
|
||||
assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id"], [])
|
||||
assert await check_shortcut_call(chat.send_gift, chat.get_bot(), "send_gift")
|
||||
assert await check_defaults_handling(chat.send_gift, chat.get_bot())
|
||||
|
||||
monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion_private)
|
||||
chat.type = chat.PRIVATE
|
||||
assert await chat.send_gift(
|
||||
gift_id="gift_id",
|
||||
text="text",
|
||||
text_parse_mode="text_parse_mode",
|
||||
text_entities="text_entities",
|
||||
)
|
||||
monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion_channel)
|
||||
chat.type = chat.CHANNEL
|
||||
monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion)
|
||||
assert await chat.send_gift(
|
||||
gift_id="gift_id",
|
||||
text="text",
|
||||
|
||||
+250
-494
@@ -16,6 +16,9 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import inspect
|
||||
from copy import deepcopy
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -29,306 +32,204 @@ from telegram import (
|
||||
BackgroundTypeFill,
|
||||
BackgroundTypePattern,
|
||||
BackgroundTypeWallpaper,
|
||||
ChatBackground,
|
||||
Dice,
|
||||
Document,
|
||||
)
|
||||
from telegram.constants import BackgroundFillType, BackgroundTypeType
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def background_fill():
|
||||
return BackgroundFill(BackgroundFillTestBase.type)
|
||||
ignored = ["self", "api_kwargs"]
|
||||
|
||||
|
||||
class BackgroundFillTestBase:
|
||||
type = BackgroundFill.SOLID
|
||||
color = 42
|
||||
top_color = 43
|
||||
bottom_color = 44
|
||||
class BFDefaults:
|
||||
color = 0
|
||||
top_color = 1
|
||||
bottom_color = 2
|
||||
rotation_angle = 45
|
||||
colors = [46, 47, 48, 49]
|
||||
colors = [0, 1, 2]
|
||||
|
||||
|
||||
class TestBackgroundFillWithoutRequest(BackgroundFillTestBase):
|
||||
def test_slots(self, background_fill):
|
||||
inst = background_fill
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_type_enum_conversion(self, background_fill):
|
||||
assert type(BackgroundFill("solid").type) is BackgroundFillType
|
||||
assert BackgroundFill("unknown").type == "unknown"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"type": "unknown"}
|
||||
transaction_partner = BackgroundFill.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "unknown"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bf_type", "subclass"),
|
||||
[
|
||||
("solid", BackgroundFillSolid),
|
||||
("gradient", BackgroundFillGradient),
|
||||
("freeform_gradient", BackgroundFillFreeformGradient),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, bf_type, subclass):
|
||||
json_dict = {
|
||||
"type": bf_type,
|
||||
"color": self.color,
|
||||
"top_color": self.top_color,
|
||||
"bottom_color": self.bottom_color,
|
||||
"rotation_angle": self.rotation_angle,
|
||||
"colors": self.colors,
|
||||
}
|
||||
bf = BackgroundFill.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(bf) is subclass
|
||||
assert set(bf.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
|
||||
"type"
|
||||
}
|
||||
assert bf.type == bf_type
|
||||
|
||||
def test_to_dict(self, background_fill):
|
||||
assert background_fill.to_dict() == {"type": background_fill.type}
|
||||
|
||||
def test_equality(self, background_fill):
|
||||
a = background_fill
|
||||
b = BackgroundFill(self.type)
|
||||
c = BackgroundFill("unknown")
|
||||
d = Dice(5, "test")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
def background_fill_solid():
|
||||
return BackgroundFillSolid(BFDefaults.color)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def background_fill_gradient():
|
||||
return BackgroundFillGradient(
|
||||
TestBackgroundFillGradientWithoutRequest.top_color,
|
||||
TestBackgroundFillGradientWithoutRequest.bottom_color,
|
||||
TestBackgroundFillGradientWithoutRequest.rotation_angle,
|
||||
BFDefaults.top_color, BFDefaults.bottom_color, BFDefaults.rotation_angle
|
||||
)
|
||||
|
||||
|
||||
class TestBackgroundFillGradientWithoutRequest(BackgroundFillTestBase):
|
||||
type = BackgroundFill.GRADIENT
|
||||
|
||||
def test_slots(self, background_fill_gradient):
|
||||
inst = background_fill_gradient
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {
|
||||
"top_color": self.top_color,
|
||||
"bottom_color": self.bottom_color,
|
||||
"rotation_angle": self.rotation_angle,
|
||||
}
|
||||
transaction_partner = BackgroundFillGradient.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "gradient"
|
||||
|
||||
def test_to_dict(self, background_fill_gradient):
|
||||
assert background_fill_gradient.to_dict() == {
|
||||
"type": background_fill_gradient.type,
|
||||
"top_color": self.top_color,
|
||||
"bottom_color": self.bottom_color,
|
||||
"rotation_angle": self.rotation_angle,
|
||||
}
|
||||
|
||||
def test_equality(self, background_fill_gradient):
|
||||
a = background_fill_gradient
|
||||
b = BackgroundFillGradient(
|
||||
self.top_color,
|
||||
self.bottom_color,
|
||||
self.rotation_angle,
|
||||
)
|
||||
c = BackgroundFillGradient(
|
||||
self.top_color + 1,
|
||||
self.bottom_color + 1,
|
||||
self.rotation_angle + 1,
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def background_fill_freeform_gradient():
|
||||
return BackgroundFillFreeformGradient(
|
||||
TestBackgroundFillFreeformGradientWithoutRequest.colors,
|
||||
return BackgroundFillFreeformGradient(BFDefaults.colors)
|
||||
|
||||
|
||||
class BTDefaults:
|
||||
document = Document(1, 2)
|
||||
fill = BackgroundFillSolid(color=0)
|
||||
dark_theme_dimming = 20
|
||||
is_blurred = True
|
||||
is_moving = False
|
||||
intensity = 90
|
||||
is_inverted = False
|
||||
theme_name = "ice"
|
||||
|
||||
|
||||
def background_type_fill():
|
||||
return BackgroundTypeFill(BTDefaults.fill, BTDefaults.dark_theme_dimming)
|
||||
|
||||
|
||||
def background_type_wallpaper():
|
||||
return BackgroundTypeWallpaper(
|
||||
BTDefaults.document,
|
||||
BTDefaults.dark_theme_dimming,
|
||||
BTDefaults.is_blurred,
|
||||
BTDefaults.is_moving,
|
||||
)
|
||||
|
||||
|
||||
class TestBackgroundFillFreeformGradientWithoutRequest(BackgroundFillTestBase):
|
||||
type = BackgroundFill.FREEFORM_GRADIENT
|
||||
def background_type_pattern():
|
||||
return BackgroundTypePattern(
|
||||
BTDefaults.document,
|
||||
BTDefaults.fill,
|
||||
BTDefaults.intensity,
|
||||
BTDefaults.is_inverted,
|
||||
BTDefaults.is_moving,
|
||||
)
|
||||
|
||||
def test_slots(self, background_fill_freeform_gradient):
|
||||
inst = background_fill_freeform_gradient
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"colors": self.colors}
|
||||
transaction_partner = BackgroundFillFreeformGradient.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "freeform_gradient"
|
||||
def background_type_chat_theme():
|
||||
return BackgroundTypeChatTheme(BTDefaults.theme_name)
|
||||
|
||||
def test_to_dict(self, background_fill_freeform_gradient):
|
||||
assert background_fill_freeform_gradient.to_dict() == {
|
||||
"type": background_fill_freeform_gradient.type,
|
||||
"colors": self.colors,
|
||||
}
|
||||
|
||||
def test_equality(self, background_fill_freeform_gradient):
|
||||
a = background_fill_freeform_gradient
|
||||
b = BackgroundFillFreeformGradient(self.colors)
|
||||
c = BackgroundFillFreeformGradient([color + 1 for color in self.colors])
|
||||
d = Dice(5, "test")
|
||||
def make_json_dict(
|
||||
instance: Union[BackgroundType, BackgroundFill], include_optional_args: bool = False
|
||||
) -> dict:
|
||||
"""Used to make the json dict which we use for testing de_json. Similar to iter_args()"""
|
||||
json_dict = {"type": instance.type}
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored: # ignore irrelevant params
|
||||
continue
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
val = getattr(instance, param.name)
|
||||
# Compulsory args-
|
||||
if param.default is inspect.Parameter.empty:
|
||||
if hasattr(val, "to_dict"): # convert the user object or any future ones to dict.
|
||||
val = val.to_dict()
|
||||
json_dict[param.name] = val
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
# If we want to test all args (for de_json)-
|
||||
elif param.default is not inspect.Parameter.empty and include_optional_args:
|
||||
json_dict[param.name] = val
|
||||
return json_dict
|
||||
|
||||
|
||||
def iter_args(
|
||||
instance: Union[BackgroundType, BackgroundFill],
|
||||
de_json_inst: Union[BackgroundType, BackgroundFill],
|
||||
include_optional: bool = False,
|
||||
):
|
||||
"""
|
||||
We accept both the regular instance and de_json created instance and iterate over them for
|
||||
easy one line testing later one.
|
||||
"""
|
||||
yield instance.type, de_json_inst.type # yield this here cause it's not available in sig.
|
||||
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored:
|
||||
continue
|
||||
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
|
||||
if (
|
||||
param.default is not inspect.Parameter.empty and include_optional
|
||||
) or param.default is inspect.Parameter.empty:
|
||||
yield inst_at, json_at
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def background_fill_solid():
|
||||
return BackgroundFillSolid(TestBackgroundFillSolidWithoutRequest.color)
|
||||
def background_type(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
class TestBackgroundFillSolidWithoutRequest(BackgroundFillTestBase):
|
||||
type = BackgroundFill.SOLID
|
||||
|
||||
def test_slots(self, background_fill_solid):
|
||||
inst = background_fill_solid
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"color": self.color}
|
||||
transaction_partner = BackgroundFillSolid.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "solid"
|
||||
|
||||
def test_to_dict(self, background_fill_solid):
|
||||
assert background_fill_solid.to_dict() == {
|
||||
"type": background_fill_solid.type,
|
||||
"color": self.color,
|
||||
}
|
||||
|
||||
def test_equality(self, background_fill_solid):
|
||||
a = background_fill_solid
|
||||
b = BackgroundFillSolid(self.color)
|
||||
c = BackgroundFillSolid(self.color + 1)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def background_type():
|
||||
return BackgroundType(BackgroundTypeTestBase.type)
|
||||
|
||||
|
||||
class BackgroundTypeTestBase:
|
||||
type = BackgroundType.WALLPAPER
|
||||
fill = BackgroundFillSolid(42)
|
||||
dark_theme_dimming = 43
|
||||
document = Document("file_id", "file_unique_id", "file_name", 42)
|
||||
is_blurred = True
|
||||
is_moving = True
|
||||
intensity = 45
|
||||
is_inverted = True
|
||||
theme_name = "test theme name"
|
||||
|
||||
|
||||
class TestBackgroundTypeWithoutRequest(BackgroundTypeTestBase):
|
||||
def test_slots(self, background_type):
|
||||
@pytest.mark.parametrize(
|
||||
"background_type",
|
||||
[
|
||||
background_type_fill,
|
||||
background_type_wallpaper,
|
||||
background_type_pattern,
|
||||
background_type_chat_theme,
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
class TestBackgroundTypeWithoutRequest:
|
||||
def test_slot_behaviour(self, background_type):
|
||||
inst = background_type
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_type_enum_conversion(self, background_type):
|
||||
assert type(BackgroundType("wallpaper").type) is BackgroundTypeType
|
||||
assert BackgroundType("unknown").type == "unknown"
|
||||
def test_de_json_required_args(self, offline_bot, background_type):
|
||||
cls = background_type.__class__
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"type": "unknown"}
|
||||
transaction_partner = BackgroundType.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "unknown"
|
||||
json_dict = make_json_dict(background_type)
|
||||
const_background_type = BackgroundType.de_json(json_dict, offline_bot)
|
||||
assert const_background_type.api_kwargs == {}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bt_type", "subclass"),
|
||||
[
|
||||
("wallpaper", BackgroundTypeWallpaper),
|
||||
("fill", BackgroundTypeFill),
|
||||
("pattern", BackgroundTypePattern),
|
||||
("chat_theme", BackgroundTypeChatTheme),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, bt_type, subclass):
|
||||
json_dict = {
|
||||
"type": bt_type,
|
||||
"fill": self.fill.to_dict(),
|
||||
"dark_theme_dimming": self.dark_theme_dimming,
|
||||
"document": self.document.to_dict(),
|
||||
"is_blurred": self.is_blurred,
|
||||
"is_moving": self.is_moving,
|
||||
"intensity": self.intensity,
|
||||
"is_inverted": self.is_inverted,
|
||||
"theme_name": self.theme_name,
|
||||
}
|
||||
bt = BackgroundType.de_json(json_dict, offline_bot)
|
||||
assert isinstance(const_background_type, BackgroundType)
|
||||
assert isinstance(const_background_type, cls)
|
||||
for bg_type_at, const_bg_type_at in iter_args(background_type, const_background_type):
|
||||
assert bg_type_at == const_bg_type_at
|
||||
|
||||
assert type(bt) is subclass
|
||||
assert set(bt.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
|
||||
"type"
|
||||
}
|
||||
assert bt.type == bt_type
|
||||
def test_de_json_all_args(self, offline_bot, background_type):
|
||||
json_dict = make_json_dict(background_type, include_optional_args=True)
|
||||
const_background_type = BackgroundType.de_json(json_dict, offline_bot)
|
||||
|
||||
assert const_background_type.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_background_type, BackgroundType)
|
||||
assert isinstance(const_background_type, background_type.__class__)
|
||||
for bg_type_at, const_bg_type_at in iter_args(
|
||||
background_type, const_background_type, True
|
||||
):
|
||||
assert bg_type_at == const_bg_type_at
|
||||
|
||||
def test_de_json_invalid_type(self, background_type, offline_bot):
|
||||
json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name}
|
||||
background_type = BackgroundType.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(background_type) is BackgroundType
|
||||
assert background_type.type == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, background_type, offline_bot, chat_id):
|
||||
"""This makes sure that e.g. BackgroundTypeFill(data, offline_bot) never returns a
|
||||
BackgroundTypeWallpaper instance."""
|
||||
cls = background_type.__class__
|
||||
json_dict = make_json_dict(background_type, True)
|
||||
assert type(cls.de_json(json_dict, offline_bot)) is cls
|
||||
|
||||
def test_to_dict(self, background_type):
|
||||
assert background_type.to_dict() == {"type": background_type.type}
|
||||
bg_type_dict = background_type.to_dict()
|
||||
|
||||
assert isinstance(bg_type_dict, dict)
|
||||
assert bg_type_dict["type"] == background_type.type
|
||||
|
||||
for slot in background_type.__slots__: # additional verification for the optional args
|
||||
if slot in ("fill", "document"):
|
||||
assert (getattr(background_type, slot)).to_dict() == bg_type_dict[slot]
|
||||
continue
|
||||
assert getattr(background_type, slot) == bg_type_dict[slot]
|
||||
|
||||
def test_equality(self, background_type):
|
||||
a = background_type
|
||||
b = BackgroundType(self.type)
|
||||
c = BackgroundType("unknown")
|
||||
d = Dice(5, "test")
|
||||
a = BackgroundType(type="type")
|
||||
b = BackgroundType(type="type")
|
||||
c = background_type
|
||||
d = deepcopy(background_type)
|
||||
e = Dice(4, "emoji")
|
||||
sig = inspect.signature(background_type.__class__.__init__)
|
||||
params = [
|
||||
"random" for param in sig.parameters.values() if param.name not in [*ignored, "type"]
|
||||
]
|
||||
f = background_type.__class__(*params)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -339,42 +240,102 @@ class TestBackgroundTypeWithoutRequest(BackgroundTypeTestBase):
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
assert f != c
|
||||
assert hash(f) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def background_type_fill():
|
||||
return BackgroundTypeFill(
|
||||
fill=TestBackgroundTypeFillWithoutRequest.fill,
|
||||
dark_theme_dimming=TestBackgroundTypeFillWithoutRequest.dark_theme_dimming,
|
||||
)
|
||||
def background_fill(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
class TestBackgroundTypeFillWithoutRequest(BackgroundTypeTestBase):
|
||||
type = BackgroundType.FILL
|
||||
|
||||
def test_slots(self, background_type_fill):
|
||||
inst = background_type_fill
|
||||
@pytest.mark.parametrize(
|
||||
"background_fill",
|
||||
[
|
||||
background_fill_solid,
|
||||
background_fill_gradient,
|
||||
background_fill_freeform_gradient,
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
class TestBackgroundFillWithoutRequest:
|
||||
def test_slot_behaviour(self, background_fill):
|
||||
inst = background_fill
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"fill": self.fill.to_dict(), "dark_theme_dimming": self.dark_theme_dimming}
|
||||
transaction_partner = BackgroundTypeFill.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "fill"
|
||||
def test_de_json_required_args(self, offline_bot, background_fill):
|
||||
cls = background_fill.__class__
|
||||
|
||||
def test_to_dict(self, background_type_fill):
|
||||
assert background_type_fill.to_dict() == {
|
||||
"type": background_type_fill.type,
|
||||
"fill": self.fill.to_dict(),
|
||||
"dark_theme_dimming": self.dark_theme_dimming,
|
||||
}
|
||||
json_dict = make_json_dict(background_fill)
|
||||
const_background_fill = BackgroundFill.de_json(json_dict, offline_bot)
|
||||
assert const_background_fill.api_kwargs == {}
|
||||
|
||||
def test_equality(self, background_type_fill):
|
||||
a = background_type_fill
|
||||
b = BackgroundTypeFill(self.fill, self.dark_theme_dimming)
|
||||
c = BackgroundTypeFill(BackgroundFillSolid(43), 44)
|
||||
d = Dice(5, "test")
|
||||
assert isinstance(const_background_fill, BackgroundFill)
|
||||
assert isinstance(const_background_fill, cls)
|
||||
for bg_fill_at, const_bg_fill_at in iter_args(background_fill, const_background_fill):
|
||||
assert bg_fill_at == const_bg_fill_at
|
||||
|
||||
def test_de_json_all_args(self, offline_bot, background_fill):
|
||||
json_dict = make_json_dict(background_fill, include_optional_args=True)
|
||||
const_background_fill = BackgroundFill.de_json(json_dict, offline_bot)
|
||||
|
||||
assert const_background_fill.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_background_fill, BackgroundFill)
|
||||
assert isinstance(const_background_fill, background_fill.__class__)
|
||||
for bg_fill_at, const_bg_fill_at in iter_args(
|
||||
background_fill, const_background_fill, True
|
||||
):
|
||||
assert bg_fill_at == const_bg_fill_at
|
||||
|
||||
def test_de_json_invalid_type(self, background_fill, offline_bot):
|
||||
json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name}
|
||||
background_fill = BackgroundFill.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(background_fill) is BackgroundFill
|
||||
assert background_fill.type == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, background_fill, offline_bot):
|
||||
"""This makes sure that e.g. BackgroundFillSolid(data, offline_bot) never returns a
|
||||
BackgroundFillGradient instance."""
|
||||
cls = background_fill.__class__
|
||||
json_dict = make_json_dict(background_fill, True)
|
||||
assert type(cls.de_json(json_dict, offline_bot)) is cls
|
||||
|
||||
def test_to_dict(self, background_fill):
|
||||
bg_fill_dict = background_fill.to_dict()
|
||||
|
||||
assert isinstance(bg_fill_dict, dict)
|
||||
assert bg_fill_dict["type"] == background_fill.type
|
||||
|
||||
for slot in background_fill.__slots__: # additional verification for the optional args
|
||||
if slot == "colors":
|
||||
assert getattr(background_fill, slot) == tuple(bg_fill_dict[slot])
|
||||
continue
|
||||
assert getattr(background_fill, slot) == bg_fill_dict[slot]
|
||||
|
||||
def test_equality(self, background_fill):
|
||||
a = BackgroundFill(type="type")
|
||||
b = BackgroundFill(type="type")
|
||||
c = background_fill
|
||||
d = deepcopy(background_fill)
|
||||
e = Dice(4, "emoji")
|
||||
sig = inspect.signature(background_fill.__class__.__init__)
|
||||
params = [
|
||||
"random" for param in sig.parameters.values() if param.name not in [*ignored, "type"]
|
||||
]
|
||||
f = background_fill.__class__(*params)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -385,219 +346,14 @@ class TestBackgroundTypeFillWithoutRequest(BackgroundTypeTestBase):
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
@pytest.fixture
|
||||
def background_type_pattern():
|
||||
return BackgroundTypePattern(
|
||||
TestBackgroundTypePatternWithoutRequest.document,
|
||||
TestBackgroundTypePatternWithoutRequest.fill,
|
||||
TestBackgroundTypePatternWithoutRequest.intensity,
|
||||
TestBackgroundTypePatternWithoutRequest.is_inverted,
|
||||
TestBackgroundTypePatternWithoutRequest.is_moving,
|
||||
)
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
class TestBackgroundTypePatternWithoutRequest(BackgroundTypeTestBase):
|
||||
type = BackgroundType.PATTERN
|
||||
|
||||
def test_slots(self, background_type_pattern):
|
||||
inst = background_type_pattern
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {
|
||||
"document": self.document.to_dict(),
|
||||
"fill": self.fill.to_dict(),
|
||||
"intensity": self.intensity,
|
||||
"is_inverted": self.is_inverted,
|
||||
"is_moving": self.is_moving,
|
||||
}
|
||||
transaction_partner = BackgroundTypePattern.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "pattern"
|
||||
|
||||
def test_to_dict(self, background_type_pattern):
|
||||
assert background_type_pattern.to_dict() == {
|
||||
"type": background_type_pattern.type,
|
||||
"document": self.document.to_dict(),
|
||||
"fill": self.fill.to_dict(),
|
||||
"intensity": self.intensity,
|
||||
"is_inverted": self.is_inverted,
|
||||
"is_moving": self.is_moving,
|
||||
}
|
||||
|
||||
def test_equality(self, background_type_pattern):
|
||||
a = background_type_pattern
|
||||
b = BackgroundTypePattern(
|
||||
self.document,
|
||||
self.fill,
|
||||
self.intensity,
|
||||
)
|
||||
c = BackgroundTypePattern(
|
||||
Document("other", "other", "file_name", 43),
|
||||
False,
|
||||
False,
|
||||
44,
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def background_type_chat_theme():
|
||||
return BackgroundTypeChatTheme(
|
||||
TestBackgroundTypeChatThemeWithoutRequest.theme_name,
|
||||
)
|
||||
|
||||
|
||||
class TestBackgroundTypeChatThemeWithoutRequest(BackgroundTypeTestBase):
|
||||
type = BackgroundType.CHAT_THEME
|
||||
|
||||
def test_slots(self, background_type_chat_theme):
|
||||
inst = background_type_chat_theme
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"theme_name": self.theme_name}
|
||||
transaction_partner = BackgroundTypeChatTheme.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "chat_theme"
|
||||
|
||||
def test_to_dict(self, background_type_chat_theme):
|
||||
assert background_type_chat_theme.to_dict() == {
|
||||
"type": background_type_chat_theme.type,
|
||||
"theme_name": self.theme_name,
|
||||
}
|
||||
|
||||
def test_equality(self, background_type_chat_theme):
|
||||
a = background_type_chat_theme
|
||||
b = BackgroundTypeChatTheme(self.theme_name)
|
||||
c = BackgroundTypeChatTheme("other")
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def background_type_wallpaper():
|
||||
return BackgroundTypeWallpaper(
|
||||
TestBackgroundTypeWallpaperWithoutRequest.document,
|
||||
TestBackgroundTypeWallpaperWithoutRequest.dark_theme_dimming,
|
||||
TestBackgroundTypeWallpaperWithoutRequest.is_blurred,
|
||||
TestBackgroundTypeWallpaperWithoutRequest.is_moving,
|
||||
)
|
||||
|
||||
|
||||
class TestBackgroundTypeWallpaperWithoutRequest(BackgroundTypeTestBase):
|
||||
type = BackgroundType.WALLPAPER
|
||||
|
||||
def test_slots(self, background_type_wallpaper):
|
||||
inst = background_type_wallpaper
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {
|
||||
"document": self.document.to_dict(),
|
||||
"dark_theme_dimming": self.dark_theme_dimming,
|
||||
"is_blurred": self.is_blurred,
|
||||
"is_moving": self.is_moving,
|
||||
}
|
||||
transaction_partner = BackgroundTypeWallpaper.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "wallpaper"
|
||||
|
||||
def test_to_dict(self, background_type_wallpaper):
|
||||
assert background_type_wallpaper.to_dict() == {
|
||||
"type": background_type_wallpaper.type,
|
||||
"document": self.document.to_dict(),
|
||||
"dark_theme_dimming": self.dark_theme_dimming,
|
||||
"is_blurred": self.is_blurred,
|
||||
"is_moving": self.is_moving,
|
||||
}
|
||||
|
||||
def test_equality(self, background_type_wallpaper):
|
||||
a = background_type_wallpaper
|
||||
b = BackgroundTypeWallpaper(
|
||||
self.document,
|
||||
self.dark_theme_dimming,
|
||||
self.is_blurred,
|
||||
self.is_moving,
|
||||
)
|
||||
c = BackgroundTypeWallpaper(
|
||||
Document("other", "other", "file_name", 43),
|
||||
44,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def chat_background():
|
||||
return ChatBackground(ChatBackgroundTestBase.type)
|
||||
|
||||
|
||||
class ChatBackgroundTestBase:
|
||||
type = BackgroundTypeFill(BackgroundFillSolid(42), 43)
|
||||
|
||||
|
||||
class TestChatBackgroundWithoutRequest(ChatBackgroundTestBase):
|
||||
def test_slots(self, chat_background):
|
||||
inst = chat_background
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"type": self.type.to_dict()}
|
||||
transaction_partner = ChatBackground.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == self.type
|
||||
|
||||
def test_to_dict(self, chat_background):
|
||||
assert chat_background.to_dict() == {"type": chat_background.type.to_dict()}
|
||||
|
||||
def test_equality(self, chat_background):
|
||||
a = chat_background
|
||||
b = ChatBackground(self.type)
|
||||
c = ChatBackground(BackgroundTypeFill(BackgroundFillSolid(43), 44))
|
||||
d = Dice(5, "test")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
assert f != c
|
||||
assert hash(f) != hash(c)
|
||||
|
||||
+244
-263
@@ -16,6 +16,8 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
|
||||
import datetime as dtm
|
||||
import inspect
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -41,231 +43,27 @@ from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
class ChatBoostDefaults:
|
||||
source = ChatBoostSource.PREMIUM
|
||||
chat_id = 1
|
||||
boost_id = "2"
|
||||
giveaway_message_id = 3
|
||||
is_unclaimed = False
|
||||
chat = Chat(1, "group")
|
||||
user = User(1, "user", False)
|
||||
date = dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0)
|
||||
date = to_timestamp(dtm.datetime.utcnow())
|
||||
default_source = ChatBoostSourcePremium(user)
|
||||
prize_star_count = 99
|
||||
boost = ChatBoost(
|
||||
boost_id=boost_id,
|
||||
add_date=date,
|
||||
expiration_date=date,
|
||||
source=default_source,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_boost_source():
|
||||
return ChatBoostSource(
|
||||
source=ChatBoostDefaults.source,
|
||||
def chat_boost_removed():
|
||||
return ChatBoostRemoved(
|
||||
chat=ChatBoostDefaults.chat,
|
||||
boost_id=ChatBoostDefaults.boost_id,
|
||||
remove_date=ChatBoostDefaults.date,
|
||||
source=ChatBoostDefaults.default_source,
|
||||
)
|
||||
|
||||
|
||||
class TestChatBoostSourceWithoutRequest(ChatBoostDefaults):
|
||||
def test_slot_behaviour(self, chat_boost_source):
|
||||
inst = chat_boost_source
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_type_enum_conversion(self, chat_boost_source):
|
||||
assert type(ChatBoostSource("premium").source) is ChatBoostSources
|
||||
assert ChatBoostSource("unknown").source == "unknown"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"source": "unknown",
|
||||
}
|
||||
cbs = ChatBoostSource.de_json(json_dict, offline_bot)
|
||||
|
||||
assert cbs.api_kwargs == {}
|
||||
assert cbs.source == "unknown"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cb_source", "subclass"),
|
||||
[
|
||||
("premium", ChatBoostSourcePremium),
|
||||
("gift_code", ChatBoostSourceGiftCode),
|
||||
("giveaway", ChatBoostSourceGiveaway),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, cb_source, subclass):
|
||||
json_dict = {
|
||||
"source": cb_source,
|
||||
"user": ChatBoostDefaults.user.to_dict(),
|
||||
"giveaway_message_id": ChatBoostDefaults.giveaway_message_id,
|
||||
}
|
||||
cbs = ChatBoostSource.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(cbs) is subclass
|
||||
assert set(cbs.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
|
||||
"source"
|
||||
}
|
||||
assert cbs.source == cb_source
|
||||
|
||||
def test_to_dict(self, chat_boost_source):
|
||||
chat_boost_source_dict = chat_boost_source.to_dict()
|
||||
|
||||
assert isinstance(chat_boost_source_dict, dict)
|
||||
assert chat_boost_source_dict["source"] == chat_boost_source.source
|
||||
|
||||
def test_equality(self, chat_boost_source):
|
||||
a = chat_boost_source
|
||||
b = ChatBoostSource(source=ChatBoostDefaults.source)
|
||||
c = ChatBoostSource(source="unknown")
|
||||
d = Dice(5, "test")
|
||||
|
||||
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 chat_boost_source_premium():
|
||||
return ChatBoostSourcePremium(
|
||||
user=TestChatBoostSourcePremiumWithoutRequest.user,
|
||||
)
|
||||
|
||||
|
||||
class TestChatBoostSourcePremiumWithoutRequest(ChatBoostDefaults):
|
||||
source = ChatBoostSources.PREMIUM
|
||||
|
||||
def test_slot_behaviour(self, chat_boost_source_premium):
|
||||
inst = chat_boost_source_premium
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"user": self.user.to_dict(),
|
||||
}
|
||||
cbsp = ChatBoostSourcePremium.de_json(json_dict, offline_bot)
|
||||
|
||||
assert cbsp.api_kwargs == {}
|
||||
assert cbsp.user == self.user
|
||||
|
||||
def test_to_dict(self, chat_boost_source_premium):
|
||||
chat_boost_source_premium_dict = chat_boost_source_premium.to_dict()
|
||||
|
||||
assert isinstance(chat_boost_source_premium_dict, dict)
|
||||
assert chat_boost_source_premium_dict["source"] == self.source
|
||||
assert chat_boost_source_premium_dict["user"] == self.user.to_dict()
|
||||
|
||||
def test_equality(self, chat_boost_source_premium):
|
||||
a = chat_boost_source_premium
|
||||
b = ChatBoostSourcePremium(user=self.user)
|
||||
c = Dice(5, "test")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_boost_source_gift_code():
|
||||
return ChatBoostSourceGiftCode(
|
||||
user=TestChatBoostSourceGiftCodeWithoutRequest.user,
|
||||
)
|
||||
|
||||
|
||||
class TestChatBoostSourceGiftCodeWithoutRequest(ChatBoostDefaults):
|
||||
source = ChatBoostSources.GIFT_CODE
|
||||
|
||||
def test_slot_behaviour(self, chat_boost_source_gift_code):
|
||||
inst = chat_boost_source_gift_code
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"user": self.user.to_dict(),
|
||||
}
|
||||
cbsgc = ChatBoostSourceGiftCode.de_json(json_dict, offline_bot)
|
||||
|
||||
assert cbsgc.api_kwargs == {}
|
||||
assert cbsgc.user == self.user
|
||||
|
||||
def test_to_dict(self, chat_boost_source_gift_code):
|
||||
chat_boost_source_gift_code_dict = chat_boost_source_gift_code.to_dict()
|
||||
|
||||
assert isinstance(chat_boost_source_gift_code_dict, dict)
|
||||
assert chat_boost_source_gift_code_dict["source"] == self.source
|
||||
assert chat_boost_source_gift_code_dict["user"] == self.user.to_dict()
|
||||
|
||||
def test_equality(self, chat_boost_source_gift_code):
|
||||
a = chat_boost_source_gift_code
|
||||
b = ChatBoostSourceGiftCode(user=self.user)
|
||||
c = Dice(5, "test")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_boost_source_giveaway():
|
||||
return ChatBoostSourceGiveaway(
|
||||
user=TestChatBoostSourceGiveawayWithoutRequest.user,
|
||||
giveaway_message_id=TestChatBoostSourceGiveawayWithoutRequest.giveaway_message_id,
|
||||
)
|
||||
|
||||
|
||||
class TestChatBoostSourceGiveawayWithoutRequest(ChatBoostDefaults):
|
||||
source = ChatBoostSources.GIVEAWAY
|
||||
|
||||
def test_slot_behaviour(self, chat_boost_source_giveaway):
|
||||
inst = chat_boost_source_giveaway
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"user": self.user.to_dict(),
|
||||
"giveaway_message_id": self.giveaway_message_id,
|
||||
}
|
||||
cbsg = ChatBoostSourceGiveaway.de_json(json_dict, offline_bot)
|
||||
|
||||
assert cbsg.api_kwargs == {}
|
||||
assert cbsg.user == self.user
|
||||
assert cbsg.giveaway_message_id == self.giveaway_message_id
|
||||
|
||||
def test_to_dict(self, chat_boost_source_giveaway):
|
||||
chat_boost_source_giveaway_dict = chat_boost_source_giveaway.to_dict()
|
||||
|
||||
assert isinstance(chat_boost_source_giveaway_dict, dict)
|
||||
assert chat_boost_source_giveaway_dict["source"] == self.source
|
||||
assert chat_boost_source_giveaway_dict["user"] == self.user.to_dict()
|
||||
assert chat_boost_source_giveaway_dict["giveaway_message_id"] == self.giveaway_message_id
|
||||
|
||||
def test_equality(self, chat_boost_source_giveaway):
|
||||
a = chat_boost_source_giveaway
|
||||
b = ChatBoostSourceGiveaway(user=self.user, giveaway_message_id=self.giveaway_message_id)
|
||||
c = Dice(5, "test")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_boost():
|
||||
return ChatBoost(
|
||||
@@ -276,6 +74,189 @@ def chat_boost():
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_boost_updated(chat_boost):
|
||||
return ChatBoostUpdated(
|
||||
chat=ChatBoostDefaults.chat,
|
||||
boost=chat_boost,
|
||||
)
|
||||
|
||||
|
||||
def chat_boost_source_gift_code():
|
||||
return ChatBoostSourceGiftCode(
|
||||
user=ChatBoostDefaults.user,
|
||||
)
|
||||
|
||||
|
||||
def chat_boost_source_giveaway():
|
||||
return ChatBoostSourceGiveaway(
|
||||
user=ChatBoostDefaults.user,
|
||||
giveaway_message_id=ChatBoostDefaults.giveaway_message_id,
|
||||
is_unclaimed=ChatBoostDefaults.is_unclaimed,
|
||||
prize_star_count=ChatBoostDefaults.prize_star_count,
|
||||
)
|
||||
|
||||
|
||||
def chat_boost_source_premium():
|
||||
return ChatBoostSourcePremium(
|
||||
user=ChatBoostDefaults.user,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def user_chat_boosts(chat_boost):
|
||||
return UserChatBoosts(
|
||||
boosts=[chat_boost],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_boost_source(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
ignored = ["self", "api_kwargs"]
|
||||
|
||||
|
||||
def make_json_dict(instance: ChatBoostSource, include_optional_args: bool = False) -> dict:
|
||||
"""Used to make the json dict which we use for testing de_json. Similar to iter_args()"""
|
||||
json_dict = {"source": instance.source}
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored: # ignore irrelevant params
|
||||
continue
|
||||
|
||||
val = getattr(instance, param.name)
|
||||
if hasattr(val, "to_dict"): # convert the user object or any future ones to dict.
|
||||
val = val.to_dict()
|
||||
json_dict[param.name] = val
|
||||
|
||||
return json_dict
|
||||
|
||||
|
||||
def iter_args(
|
||||
instance: ChatBoostSource, de_json_inst: ChatBoostSource, include_optional: bool = False
|
||||
):
|
||||
"""
|
||||
We accept both the regular instance and de_json created instance and iterate over them for
|
||||
easy one line testing later one.
|
||||
"""
|
||||
yield instance.source, de_json_inst.source # yield this here cause it's not available in sig.
|
||||
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored:
|
||||
continue
|
||||
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
|
||||
if isinstance(json_at, dtm.datetime): # Convert dtm to int
|
||||
json_at = to_timestamp(json_at)
|
||||
if (
|
||||
param.default is not inspect.Parameter.empty and include_optional
|
||||
) or param.default is inspect.Parameter.empty:
|
||||
yield inst_at, json_at
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"chat_boost_source",
|
||||
[
|
||||
chat_boost_source_gift_code,
|
||||
chat_boost_source_giveaway,
|
||||
chat_boost_source_premium,
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
class TestChatBoostSourceTypesWithoutRequest:
|
||||
def test_slot_behaviour(self, chat_boost_source):
|
||||
inst = chat_boost_source
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json_required_args(self, offline_bot, chat_boost_source):
|
||||
cls = chat_boost_source.__class__
|
||||
|
||||
json_dict = make_json_dict(chat_boost_source)
|
||||
const_boost_source = ChatBoostSource.de_json(json_dict, offline_bot)
|
||||
assert const_boost_source.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_boost_source, ChatBoostSource)
|
||||
assert isinstance(const_boost_source, cls)
|
||||
for chat_mem_type_at, const_chat_mem_at in iter_args(
|
||||
chat_boost_source, const_boost_source
|
||||
):
|
||||
assert chat_mem_type_at == const_chat_mem_at
|
||||
|
||||
def test_de_json_all_args(self, offline_bot, chat_boost_source):
|
||||
json_dict = make_json_dict(chat_boost_source, include_optional_args=True)
|
||||
const_boost_source = ChatBoostSource.de_json(json_dict, offline_bot)
|
||||
assert const_boost_source.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_boost_source, ChatBoostSource)
|
||||
assert isinstance(const_boost_source, chat_boost_source.__class__)
|
||||
for c_mem_type_at, const_c_mem_at in iter_args(
|
||||
chat_boost_source, const_boost_source, True
|
||||
):
|
||||
assert c_mem_type_at == const_c_mem_at
|
||||
|
||||
def test_de_json_invalid_source(self, chat_boost_source, offline_bot):
|
||||
json_dict = {"source": "invalid"}
|
||||
chat_boost_source = ChatBoostSource.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(chat_boost_source) is ChatBoostSource
|
||||
assert chat_boost_source.source == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, chat_boost_source, offline_bot):
|
||||
"""This makes sure that e.g. ChatBoostSourcePremium(data, offline_bot) never returns a
|
||||
ChatBoostSourceGiftCode instance."""
|
||||
cls = chat_boost_source.__class__
|
||||
json_dict = make_json_dict(chat_boost_source, True)
|
||||
assert type(cls.de_json(json_dict, offline_bot)) is cls
|
||||
|
||||
def test_to_dict(self, chat_boost_source):
|
||||
chat_boost_dict = chat_boost_source.to_dict()
|
||||
|
||||
assert isinstance(chat_boost_dict, dict)
|
||||
assert chat_boost_dict["source"] == chat_boost_source.source
|
||||
assert chat_boost_dict["user"] == chat_boost_source.user.to_dict()
|
||||
|
||||
for slot in chat_boost_source.__slots__: # additional verification for the optional args
|
||||
if slot == "user": # we already test "user" above:
|
||||
continue
|
||||
assert getattr(chat_boost_source, slot) == chat_boost_dict[slot]
|
||||
|
||||
def test_equality(self, chat_boost_source):
|
||||
a = ChatBoostSource(source="status")
|
||||
b = ChatBoostSource(source="status")
|
||||
c = chat_boost_source
|
||||
d = deepcopy(chat_boost_source)
|
||||
e = Dice(4, "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)
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
def test_enum_init(self, chat_boost_source):
|
||||
cbs = ChatBoostSource(source="foo")
|
||||
assert cbs.source == "foo"
|
||||
cbs = ChatBoostSource(source="premium")
|
||||
assert cbs.source == ChatBoostSources.PREMIUM
|
||||
|
||||
|
||||
class TestChatBoostWithoutRequest(ChatBoostDefaults):
|
||||
def test_slot_behaviour(self, chat_boost):
|
||||
inst = chat_boost
|
||||
@@ -285,24 +266,30 @@ class TestChatBoostWithoutRequest(ChatBoostDefaults):
|
||||
|
||||
def test_de_json(self, offline_bot, chat_boost):
|
||||
json_dict = {
|
||||
"boost_id": self.boost_id,
|
||||
"add_date": to_timestamp(self.date),
|
||||
"expiration_date": to_timestamp(self.date),
|
||||
"boost_id": "2",
|
||||
"add_date": self.date,
|
||||
"expiration_date": self.date,
|
||||
"source": self.default_source.to_dict(),
|
||||
}
|
||||
cb = ChatBoost.de_json(json_dict, offline_bot)
|
||||
|
||||
assert cb.api_kwargs == {}
|
||||
assert cb.boost_id == self.boost_id
|
||||
assert (cb.add_date) == self.date
|
||||
assert (cb.expiration_date) == self.date
|
||||
assert cb.source == self.default_source
|
||||
assert isinstance(cb, ChatBoost)
|
||||
assert isinstance(cb.add_date, dtm.datetime)
|
||||
assert isinstance(cb.expiration_date, dtm.datetime)
|
||||
assert isinstance(cb.source, ChatBoostSource)
|
||||
with cb._unfrozen():
|
||||
cb.add_date = to_timestamp(cb.add_date)
|
||||
cb.expiration_date = to_timestamp(cb.expiration_date)
|
||||
|
||||
# We don't compare cbu.boost to self.boost because we have to update the _id_attrs (sigh)
|
||||
for slot in cb.__slots__:
|
||||
assert getattr(cb, slot) == getattr(chat_boost, slot), f"attribute {slot} differs"
|
||||
|
||||
def test_de_json_localization(self, offline_bot, raw_bot, tz_bot):
|
||||
json_dict = {
|
||||
"boost_id": "2",
|
||||
"add_date": to_timestamp(self.date),
|
||||
"expiration_date": to_timestamp(self.date),
|
||||
"add_date": self.date,
|
||||
"expiration_date": self.date,
|
||||
"source": self.default_source.to_dict(),
|
||||
}
|
||||
|
||||
@@ -323,8 +310,8 @@ class TestChatBoostWithoutRequest(ChatBoostDefaults):
|
||||
|
||||
assert isinstance(chat_boost_dict, dict)
|
||||
assert chat_boost_dict["boost_id"] == chat_boost.boost_id
|
||||
assert chat_boost_dict["add_date"] == to_timestamp(chat_boost.add_date)
|
||||
assert chat_boost_dict["expiration_date"] == to_timestamp(chat_boost.expiration_date)
|
||||
assert chat_boost_dict["add_date"] == chat_boost.add_date
|
||||
assert chat_boost_dict["expiration_date"] == chat_boost.expiration_date
|
||||
assert chat_boost_dict["source"] == chat_boost.source.to_dict()
|
||||
|
||||
def test_equality(self):
|
||||
@@ -354,14 +341,6 @@ class TestChatBoostWithoutRequest(ChatBoostDefaults):
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_boost_updated(chat_boost):
|
||||
return ChatBoostUpdated(
|
||||
chat=ChatBoostDefaults.chat,
|
||||
boost=chat_boost,
|
||||
)
|
||||
|
||||
|
||||
class TestChatBoostUpdatedWithoutRequest(ChatBoostDefaults):
|
||||
def test_slot_behaviour(self, chat_boost_updated):
|
||||
inst = chat_boost_updated
|
||||
@@ -372,13 +351,25 @@ class TestChatBoostUpdatedWithoutRequest(ChatBoostDefaults):
|
||||
def test_de_json(self, offline_bot, chat_boost):
|
||||
json_dict = {
|
||||
"chat": self.chat.to_dict(),
|
||||
"boost": self.boost.to_dict(),
|
||||
"boost": {
|
||||
"boost_id": "2",
|
||||
"add_date": self.date,
|
||||
"expiration_date": self.date,
|
||||
"source": self.default_source.to_dict(),
|
||||
},
|
||||
}
|
||||
cbu = ChatBoostUpdated.de_json(json_dict, offline_bot)
|
||||
|
||||
assert cbu.api_kwargs == {}
|
||||
assert isinstance(cbu, ChatBoostUpdated)
|
||||
assert cbu.chat == self.chat
|
||||
assert cbu.boost == self.boost
|
||||
# We don't compare cbu.boost to chat_boost because we have to update the _id_attrs (sigh)
|
||||
with cbu.boost._unfrozen():
|
||||
cbu.boost.add_date = to_timestamp(cbu.boost.add_date)
|
||||
cbu.boost.expiration_date = to_timestamp(cbu.boost.expiration_date)
|
||||
for slot in cbu.boost.__slots__: # Assumes _id_attrs are same as slots
|
||||
assert getattr(cbu.boost, slot) == getattr(chat_boost, slot), f"attr {slot} differs"
|
||||
|
||||
# no need to test localization since that is already tested in the above class.
|
||||
|
||||
def test_to_dict(self, chat_boost_updated):
|
||||
chat_boost_updated_dict = chat_boost_updated.to_dict()
|
||||
@@ -423,16 +414,6 @@ class TestChatBoostUpdatedWithoutRequest(ChatBoostDefaults):
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_boost_removed():
|
||||
return ChatBoostRemoved(
|
||||
chat=ChatBoostDefaults.chat,
|
||||
boost_id=ChatBoostDefaults.boost_id,
|
||||
remove_date=ChatBoostDefaults.date,
|
||||
source=ChatBoostDefaults.default_source,
|
||||
)
|
||||
|
||||
|
||||
class TestChatBoostRemovedWithoutRequest(ChatBoostDefaults):
|
||||
def test_slot_behaviour(self, chat_boost_removed):
|
||||
inst = chat_boost_removed
|
||||
@@ -443,23 +424,23 @@ class TestChatBoostRemovedWithoutRequest(ChatBoostDefaults):
|
||||
def test_de_json(self, offline_bot, chat_boost_removed):
|
||||
json_dict = {
|
||||
"chat": self.chat.to_dict(),
|
||||
"boost_id": self.boost_id,
|
||||
"remove_date": to_timestamp(self.date),
|
||||
"boost_id": "2",
|
||||
"remove_date": self.date,
|
||||
"source": self.default_source.to_dict(),
|
||||
}
|
||||
cbr = ChatBoostRemoved.de_json(json_dict, offline_bot)
|
||||
|
||||
assert cbr.api_kwargs == {}
|
||||
assert isinstance(cbr, ChatBoostRemoved)
|
||||
assert cbr.chat == self.chat
|
||||
assert cbr.boost_id == self.boost_id
|
||||
assert cbr.remove_date == self.date
|
||||
assert to_timestamp(cbr.remove_date) == self.date
|
||||
assert cbr.source == self.default_source
|
||||
|
||||
def test_de_json_localization(self, offline_bot, raw_bot, tz_bot):
|
||||
json_dict = {
|
||||
"chat": self.chat.to_dict(),
|
||||
"boost_id": self.boost_id,
|
||||
"remove_date": to_timestamp(self.date),
|
||||
"boost_id": "2",
|
||||
"remove_date": self.date,
|
||||
"source": self.default_source.to_dict(),
|
||||
}
|
||||
|
||||
@@ -481,9 +462,7 @@ class TestChatBoostRemovedWithoutRequest(ChatBoostDefaults):
|
||||
assert isinstance(chat_boost_removed_dict, dict)
|
||||
assert chat_boost_removed_dict["chat"] == chat_boost_removed.chat.to_dict()
|
||||
assert chat_boost_removed_dict["boost_id"] == chat_boost_removed.boost_id
|
||||
assert chat_boost_removed_dict["remove_date"] == to_timestamp(
|
||||
chat_boost_removed.remove_date
|
||||
)
|
||||
assert chat_boost_removed_dict["remove_date"] == chat_boost_removed.remove_date
|
||||
assert chat_boost_removed_dict["source"] == chat_boost_removed.source.to_dict()
|
||||
|
||||
def test_equality(self):
|
||||
@@ -513,13 +492,6 @@ class TestChatBoostRemovedWithoutRequest(ChatBoostDefaults):
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def user_chat_boosts(chat_boost):
|
||||
return UserChatBoosts(
|
||||
boosts=[chat_boost],
|
||||
)
|
||||
|
||||
|
||||
class TestUserChatBoostsWithoutRequest(ChatBoostDefaults):
|
||||
def test_slot_behaviour(self, user_chat_boosts):
|
||||
inst = user_chat_boosts
|
||||
@@ -530,13 +502,22 @@ class TestUserChatBoostsWithoutRequest(ChatBoostDefaults):
|
||||
def test_de_json(self, offline_bot, user_chat_boosts):
|
||||
json_dict = {
|
||||
"boosts": [
|
||||
self.boost.to_dict(),
|
||||
{
|
||||
"boost_id": "2",
|
||||
"add_date": self.date,
|
||||
"expiration_date": self.date,
|
||||
"source": self.default_source.to_dict(),
|
||||
}
|
||||
]
|
||||
}
|
||||
ucb = UserChatBoosts.de_json(json_dict, offline_bot)
|
||||
|
||||
assert ucb.api_kwargs == {}
|
||||
assert ucb.boosts[0] == self.boost
|
||||
assert isinstance(ucb, UserChatBoosts)
|
||||
assert isinstance(ucb.boosts[0], ChatBoost)
|
||||
assert ucb.boosts[0].boost_id == self.boost_id
|
||||
assert to_timestamp(ucb.boosts[0].add_date) == self.date
|
||||
assert to_timestamp(ucb.boosts[0].expiration_date) == self.date
|
||||
assert ucb.boosts[0].source == self.default_source
|
||||
|
||||
def test_to_dict(self, user_chat_boosts):
|
||||
user_chat_boosts_dict = user_chat_boosts.to_dict()
|
||||
|
||||
+239
-623
@@ -34,107 +34,257 @@ from telegram import (
|
||||
User,
|
||||
)
|
||||
from telegram._utils.datetime import UTC, to_timestamp
|
||||
from telegram.constants import ChatMemberStatus
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
ignored = ["self", "api_kwargs"]
|
||||
|
||||
|
||||
class CMDefaults:
|
||||
user = User(1, "First name", False)
|
||||
custom_title: str = "PTB"
|
||||
is_anonymous: bool = True
|
||||
until_date: dtm.datetime = to_timestamp(dtm.datetime.utcnow())
|
||||
can_be_edited: bool = False
|
||||
can_change_info: bool = True
|
||||
can_post_messages: bool = True
|
||||
can_edit_messages: bool = True
|
||||
can_delete_messages: bool = True
|
||||
can_invite_users: bool = True
|
||||
can_restrict_members: bool = True
|
||||
can_pin_messages: bool = True
|
||||
can_promote_members: bool = True
|
||||
can_send_messages: bool = True
|
||||
can_send_media_messages: bool = True
|
||||
can_send_polls: bool = True
|
||||
can_send_other_messages: bool = True
|
||||
can_add_web_page_previews: bool = True
|
||||
is_member: bool = True
|
||||
can_manage_chat: bool = True
|
||||
can_manage_video_chats: bool = True
|
||||
can_manage_topics: bool = True
|
||||
can_send_audios: bool = True
|
||||
can_send_documents: bool = True
|
||||
can_send_photos: bool = True
|
||||
can_send_videos: bool = True
|
||||
can_send_video_notes: bool = True
|
||||
can_send_voice_notes: bool = True
|
||||
can_post_stories: bool = True
|
||||
can_edit_stories: bool = True
|
||||
can_delete_stories: bool = True
|
||||
|
||||
|
||||
def chat_member_owner():
|
||||
return ChatMemberOwner(CMDefaults.user, CMDefaults.is_anonymous, CMDefaults.custom_title)
|
||||
|
||||
|
||||
def chat_member_administrator():
|
||||
return ChatMemberAdministrator(
|
||||
CMDefaults.user,
|
||||
CMDefaults.can_be_edited,
|
||||
CMDefaults.is_anonymous,
|
||||
CMDefaults.can_manage_chat,
|
||||
CMDefaults.can_delete_messages,
|
||||
CMDefaults.can_manage_video_chats,
|
||||
CMDefaults.can_restrict_members,
|
||||
CMDefaults.can_promote_members,
|
||||
CMDefaults.can_change_info,
|
||||
CMDefaults.can_invite_users,
|
||||
CMDefaults.can_post_stories,
|
||||
CMDefaults.can_edit_stories,
|
||||
CMDefaults.can_delete_stories,
|
||||
CMDefaults.can_post_messages,
|
||||
CMDefaults.can_edit_messages,
|
||||
CMDefaults.can_pin_messages,
|
||||
CMDefaults.can_manage_topics,
|
||||
CMDefaults.custom_title,
|
||||
)
|
||||
|
||||
|
||||
def chat_member_member():
|
||||
return ChatMemberMember(CMDefaults.user, until_date=CMDefaults.until_date)
|
||||
|
||||
|
||||
def chat_member_restricted():
|
||||
return ChatMemberRestricted(
|
||||
CMDefaults.user,
|
||||
CMDefaults.is_member,
|
||||
CMDefaults.can_change_info,
|
||||
CMDefaults.can_invite_users,
|
||||
CMDefaults.can_pin_messages,
|
||||
CMDefaults.can_send_messages,
|
||||
CMDefaults.can_send_polls,
|
||||
CMDefaults.can_send_other_messages,
|
||||
CMDefaults.can_add_web_page_previews,
|
||||
CMDefaults.can_manage_topics,
|
||||
CMDefaults.until_date,
|
||||
CMDefaults.can_send_audios,
|
||||
CMDefaults.can_send_documents,
|
||||
CMDefaults.can_send_photos,
|
||||
CMDefaults.can_send_videos,
|
||||
CMDefaults.can_send_video_notes,
|
||||
CMDefaults.can_send_voice_notes,
|
||||
)
|
||||
|
||||
|
||||
def chat_member_left():
|
||||
return ChatMemberLeft(CMDefaults.user)
|
||||
|
||||
|
||||
def chat_member_banned():
|
||||
return ChatMemberBanned(CMDefaults.user, CMDefaults.until_date)
|
||||
|
||||
|
||||
def make_json_dict(instance: ChatMember, include_optional_args: bool = False) -> dict:
|
||||
"""Used to make the json dict which we use for testing de_json. Similar to iter_args()"""
|
||||
json_dict = {"status": instance.status}
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored: # ignore irrelevant params
|
||||
continue
|
||||
|
||||
val = getattr(instance, param.name)
|
||||
# Compulsory args-
|
||||
if param.default is inspect.Parameter.empty:
|
||||
if hasattr(val, "to_dict"): # convert the user object or any future ones to dict.
|
||||
val = val.to_dict()
|
||||
json_dict[param.name] = val
|
||||
|
||||
# If we want to test all args (for de_json)
|
||||
# or if the param is optional but for backwards compatability
|
||||
elif (
|
||||
param.default is not inspect.Parameter.empty and include_optional_args
|
||||
) or param.name in ["can_delete_stories", "can_post_stories", "can_edit_stories"]:
|
||||
json_dict[param.name] = val
|
||||
return json_dict
|
||||
|
||||
|
||||
def iter_args(instance: ChatMember, de_json_inst: ChatMember, include_optional: bool = False):
|
||||
"""
|
||||
We accept both the regular instance and de_json created instance and iterate over them for
|
||||
easy one line testing later one.
|
||||
"""
|
||||
yield instance.status, de_json_inst.status # yield this here cause it's not available in sig.
|
||||
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored:
|
||||
continue
|
||||
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
|
||||
if isinstance(json_at, dtm.datetime): # Convert dtm to int
|
||||
json_at = to_timestamp(json_at)
|
||||
if (
|
||||
param.default is not inspect.Parameter.empty and include_optional
|
||||
) or param.default is inspect.Parameter.empty:
|
||||
yield inst_at, json_at
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_member():
|
||||
return ChatMember(ChatMemberTestBase.user, ChatMemberTestBase.status)
|
||||
def chat_member_type(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
class ChatMemberTestBase:
|
||||
status = ChatMemberStatus.MEMBER
|
||||
user = User(1, "test_user", is_bot=False)
|
||||
is_anonymous = True
|
||||
custom_title = "test_title"
|
||||
can_be_edited = True
|
||||
can_manage_chat = True
|
||||
can_delete_messages = True
|
||||
can_manage_video_chats = True
|
||||
can_restrict_members = True
|
||||
can_promote_members = True
|
||||
can_change_info = True
|
||||
can_invite_users = True
|
||||
can_post_messages = True
|
||||
can_edit_messages = True
|
||||
can_pin_messages = True
|
||||
can_post_stories = True
|
||||
can_edit_stories = True
|
||||
can_delete_stories = True
|
||||
can_manage_topics = True
|
||||
until_date = dtm.datetime.now(UTC).replace(microsecond=0)
|
||||
can_send_polls = True
|
||||
can_send_other_messages = True
|
||||
can_add_web_page_previews = True
|
||||
can_send_audios = True
|
||||
can_send_documents = True
|
||||
can_send_photos = True
|
||||
can_send_videos = True
|
||||
can_send_video_notes = True
|
||||
can_send_voice_notes = True
|
||||
can_send_messages = True
|
||||
is_member = True
|
||||
|
||||
|
||||
class TestChatMemberWithoutRequest(ChatMemberTestBase):
|
||||
def test_slot_behaviour(self, chat_member):
|
||||
inst = chat_member
|
||||
@pytest.mark.parametrize(
|
||||
"chat_member_type",
|
||||
[
|
||||
chat_member_owner,
|
||||
chat_member_administrator,
|
||||
chat_member_member,
|
||||
chat_member_restricted,
|
||||
chat_member_left,
|
||||
chat_member_banned,
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
class TestChatMemberTypesWithoutRequest:
|
||||
def test_slot_behaviour(self, chat_member_type):
|
||||
inst = chat_member_type
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_status_enum_conversion(self, chat_member):
|
||||
assert type(ChatMember(ChatMemberTestBase.user, "member").status) is ChatMemberStatus
|
||||
assert ChatMember(ChatMemberTestBase.user, "unknown").status == "unknown"
|
||||
def test_de_json_required_args(self, offline_bot, chat_member_type):
|
||||
cls = chat_member_type.__class__
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"status": "unknown", "user": self.user.to_dict()}
|
||||
chat_member = ChatMember.de_json(data, offline_bot)
|
||||
assert chat_member.api_kwargs == {}
|
||||
assert chat_member.status == "unknown"
|
||||
assert chat_member.user == self.user
|
||||
json_dict = make_json_dict(chat_member_type)
|
||||
const_chat_member = ChatMember.de_json(json_dict, offline_bot)
|
||||
assert const_chat_member.api_kwargs == {}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "subclass"),
|
||||
[
|
||||
("administrator", ChatMemberAdministrator),
|
||||
("kicked", ChatMemberBanned),
|
||||
("left", ChatMemberLeft),
|
||||
("member", ChatMemberMember),
|
||||
("creator", ChatMemberOwner),
|
||||
("restricted", ChatMemberRestricted),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, status, subclass):
|
||||
json_dict = {
|
||||
"status": status,
|
||||
"user": self.user.to_dict(),
|
||||
"is_anonymous": self.is_anonymous,
|
||||
"is_member": self.is_member,
|
||||
"until_date": to_timestamp(self.until_date),
|
||||
**{name: value for name, value in inspect.getmembers(self) if name.startswith("can_")},
|
||||
}
|
||||
chat_member = ChatMember.de_json(json_dict, offline_bot)
|
||||
assert isinstance(const_chat_member, ChatMember)
|
||||
assert isinstance(const_chat_member, cls)
|
||||
for chat_mem_type_at, const_chat_mem_at in iter_args(chat_member_type, const_chat_member):
|
||||
assert chat_mem_type_at == const_chat_mem_at
|
||||
|
||||
assert type(chat_member) is subclass
|
||||
assert set(chat_member.api_kwargs.keys()) == set(json_dict.keys()) - set(
|
||||
subclass.__slots__
|
||||
) - {"status", "user"}
|
||||
assert chat_member.user == self.user
|
||||
def test_de_json_all_args(self, offline_bot, chat_member_type):
|
||||
json_dict = make_json_dict(chat_member_type, include_optional_args=True)
|
||||
const_chat_member = ChatMember.de_json(json_dict, offline_bot)
|
||||
assert const_chat_member.api_kwargs == {}
|
||||
|
||||
def test_to_dict(self, chat_member):
|
||||
assert chat_member.to_dict() == {
|
||||
"status": chat_member.status,
|
||||
"user": chat_member.user.to_dict(),
|
||||
assert isinstance(const_chat_member, ChatMember)
|
||||
assert isinstance(const_chat_member, chat_member_type.__class__)
|
||||
for c_mem_type_at, const_c_mem_at in iter_args(chat_member_type, const_chat_member, True):
|
||||
assert c_mem_type_at == const_c_mem_at
|
||||
|
||||
def test_de_json_chatmemberbanned_localization(
|
||||
self, chat_member_type, tz_bot, offline_bot, raw_bot
|
||||
):
|
||||
# We only test two classes because the other three don't have datetimes in them.
|
||||
if isinstance(
|
||||
chat_member_type, (ChatMemberBanned, ChatMemberRestricted, ChatMemberMember)
|
||||
):
|
||||
json_dict = make_json_dict(chat_member_type, include_optional_args=True)
|
||||
chatmember_raw = ChatMember.de_json(json_dict, raw_bot)
|
||||
chatmember_bot = ChatMember.de_json(json_dict, offline_bot)
|
||||
chatmember_tz = ChatMember.de_json(json_dict, tz_bot)
|
||||
|
||||
# comparing utcoffsets because comparing timezones is unpredicatable
|
||||
chatmember_offset = chatmember_tz.until_date.utcoffset()
|
||||
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(
|
||||
chatmember_tz.until_date.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
assert chatmember_raw.until_date.tzinfo == UTC
|
||||
assert chatmember_bot.until_date.tzinfo == UTC
|
||||
assert chatmember_offset == tz_bot_offset
|
||||
|
||||
def test_de_json_invalid_status(self, chat_member_type, offline_bot):
|
||||
json_dict = {"status": "invalid", "user": CMDefaults.user.to_dict()}
|
||||
chat_member_type = ChatMember.de_json(json_dict, offline_bot)
|
||||
|
||||
assert type(chat_member_type) is ChatMember
|
||||
assert chat_member_type.status == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, chat_member_type, offline_bot, chat_id):
|
||||
"""This makes sure that e.g. ChatMemberAdministrator(data, offline_bot) never returns a
|
||||
ChatMemberBanned instance."""
|
||||
cls = chat_member_type.__class__
|
||||
json_dict = make_json_dict(chat_member_type, True)
|
||||
assert type(cls.de_json(json_dict, offline_bot)) is cls
|
||||
|
||||
def test_to_dict(self, chat_member_type):
|
||||
chat_member_dict = chat_member_type.to_dict()
|
||||
|
||||
assert isinstance(chat_member_dict, dict)
|
||||
assert chat_member_dict["status"] == chat_member_type.status
|
||||
assert chat_member_dict["user"] == chat_member_type.user.to_dict()
|
||||
|
||||
for slot in chat_member_type.__slots__: # additional verification for the optional args
|
||||
assert getattr(chat_member_type, slot) == chat_member_dict[slot]
|
||||
|
||||
def test_chat_member_restricted_api_kwargs(self, chat_member_type):
|
||||
json_dict = make_json_dict(chat_member_restricted())
|
||||
json_dict["can_send_media_messages"] = "can_send_media_messages"
|
||||
chat_member_restricted_instance = ChatMember.de_json(json_dict, None)
|
||||
assert chat_member_restricted_instance.api_kwargs == {
|
||||
"can_send_media_messages": "can_send_media_messages",
|
||||
}
|
||||
|
||||
def test_equality(self, chat_member):
|
||||
a = chat_member
|
||||
b = ChatMember(self.user, self.status)
|
||||
c = ChatMember(self.user, "unknown")
|
||||
d = ChatMember(User(2, "test_bot", is_bot=True), self.status)
|
||||
e = Dice(5, "test")
|
||||
def test_equality(self, chat_member_type):
|
||||
a = ChatMember(status="status", user=CMDefaults.user)
|
||||
b = ChatMember(status="status", user=CMDefaults.user)
|
||||
c = chat_member_type
|
||||
d = deepcopy(chat_member_type)
|
||||
e = Dice(4, "emoji")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -148,542 +298,8 @@ class TestChatMemberWithoutRequest(ChatMemberTestBase):
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
@pytest.fixture
|
||||
def chat_member_administrator():
|
||||
return ChatMemberAdministrator(
|
||||
TestChatMemberAdministratorWithoutRequest.user,
|
||||
TestChatMemberAdministratorWithoutRequest.can_be_edited,
|
||||
TestChatMemberAdministratorWithoutRequest.can_change_info,
|
||||
TestChatMemberAdministratorWithoutRequest.can_delete_messages,
|
||||
TestChatMemberAdministratorWithoutRequest.can_delete_stories,
|
||||
TestChatMemberAdministratorWithoutRequest.can_edit_messages,
|
||||
TestChatMemberAdministratorWithoutRequest.can_edit_stories,
|
||||
TestChatMemberAdministratorWithoutRequest.can_invite_users,
|
||||
TestChatMemberAdministratorWithoutRequest.can_manage_chat,
|
||||
TestChatMemberAdministratorWithoutRequest.can_manage_topics,
|
||||
TestChatMemberAdministratorWithoutRequest.can_manage_video_chats,
|
||||
TestChatMemberAdministratorWithoutRequest.can_pin_messages,
|
||||
TestChatMemberAdministratorWithoutRequest.can_post_messages,
|
||||
TestChatMemberAdministratorWithoutRequest.can_post_stories,
|
||||
TestChatMemberAdministratorWithoutRequest.can_promote_members,
|
||||
TestChatMemberAdministratorWithoutRequest.can_restrict_members,
|
||||
TestChatMemberAdministratorWithoutRequest.custom_title,
|
||||
TestChatMemberAdministratorWithoutRequest.is_anonymous,
|
||||
)
|
||||
|
||||
|
||||
class TestChatMemberAdministratorWithoutRequest(ChatMemberTestBase):
|
||||
status = ChatMemberStatus.ADMINISTRATOR
|
||||
|
||||
def test_slot_behaviour(self, chat_member_administrator):
|
||||
inst = chat_member_administrator
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {
|
||||
"user": self.user.to_dict(),
|
||||
"can_be_edited": self.can_be_edited,
|
||||
"can_change_info": self.can_change_info,
|
||||
"can_delete_messages": self.can_delete_messages,
|
||||
"can_delete_stories": self.can_delete_stories,
|
||||
"can_edit_messages": self.can_edit_messages,
|
||||
"can_edit_stories": self.can_edit_stories,
|
||||
"can_invite_users": self.can_invite_users,
|
||||
"can_manage_chat": self.can_manage_chat,
|
||||
"can_manage_topics": self.can_manage_topics,
|
||||
"can_manage_video_chats": self.can_manage_video_chats,
|
||||
"can_pin_messages": self.can_pin_messages,
|
||||
"can_post_messages": self.can_post_messages,
|
||||
"can_post_stories": self.can_post_stories,
|
||||
"can_promote_members": self.can_promote_members,
|
||||
"can_restrict_members": self.can_restrict_members,
|
||||
"custom_title": self.custom_title,
|
||||
"is_anonymous": self.is_anonymous,
|
||||
}
|
||||
chat_member = ChatMemberAdministrator.de_json(data, offline_bot)
|
||||
|
||||
assert type(chat_member) is ChatMemberAdministrator
|
||||
assert chat_member.api_kwargs == {}
|
||||
|
||||
assert chat_member.user == self.user
|
||||
assert chat_member.can_be_edited == self.can_be_edited
|
||||
assert chat_member.can_change_info == self.can_change_info
|
||||
assert chat_member.can_delete_messages == self.can_delete_messages
|
||||
assert chat_member.can_delete_stories == self.can_delete_stories
|
||||
assert chat_member.can_edit_messages == self.can_edit_messages
|
||||
assert chat_member.can_edit_stories == self.can_edit_stories
|
||||
assert chat_member.can_invite_users == self.can_invite_users
|
||||
assert chat_member.can_manage_chat == self.can_manage_chat
|
||||
assert chat_member.can_manage_topics == self.can_manage_topics
|
||||
assert chat_member.can_manage_video_chats == self.can_manage_video_chats
|
||||
assert chat_member.can_pin_messages == self.can_pin_messages
|
||||
assert chat_member.can_post_messages == self.can_post_messages
|
||||
assert chat_member.can_post_stories == self.can_post_stories
|
||||
assert chat_member.can_promote_members == self.can_promote_members
|
||||
assert chat_member.can_restrict_members == self.can_restrict_members
|
||||
assert chat_member.custom_title == self.custom_title
|
||||
assert chat_member.is_anonymous == self.is_anonymous
|
||||
|
||||
def test_to_dict(self, chat_member_administrator):
|
||||
assert chat_member_administrator.to_dict() == {
|
||||
"status": chat_member_administrator.status,
|
||||
"user": chat_member_administrator.user.to_dict(),
|
||||
"can_be_edited": chat_member_administrator.can_be_edited,
|
||||
"can_change_info": chat_member_administrator.can_change_info,
|
||||
"can_delete_messages": chat_member_administrator.can_delete_messages,
|
||||
"can_delete_stories": chat_member_administrator.can_delete_stories,
|
||||
"can_edit_messages": chat_member_administrator.can_edit_messages,
|
||||
"can_edit_stories": chat_member_administrator.can_edit_stories,
|
||||
"can_invite_users": chat_member_administrator.can_invite_users,
|
||||
"can_manage_chat": chat_member_administrator.can_manage_chat,
|
||||
"can_manage_topics": chat_member_administrator.can_manage_topics,
|
||||
"can_manage_video_chats": chat_member_administrator.can_manage_video_chats,
|
||||
"can_pin_messages": chat_member_administrator.can_pin_messages,
|
||||
"can_post_messages": chat_member_administrator.can_post_messages,
|
||||
"can_post_stories": chat_member_administrator.can_post_stories,
|
||||
"can_promote_members": chat_member_administrator.can_promote_members,
|
||||
"can_restrict_members": chat_member_administrator.can_restrict_members,
|
||||
"custom_title": chat_member_administrator.custom_title,
|
||||
"is_anonymous": chat_member_administrator.is_anonymous,
|
||||
}
|
||||
|
||||
def test_equality(self, chat_member_administrator):
|
||||
a = chat_member_administrator
|
||||
b = ChatMemberAdministrator(
|
||||
User(1, "test_user", is_bot=False),
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
)
|
||||
c = ChatMemberAdministrator(
|
||||
User(1, "test_user", is_bot=False),
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def chat_member_banned():
|
||||
return ChatMemberBanned(
|
||||
TestChatMemberBannedWithoutRequest.user,
|
||||
TestChatMemberBannedWithoutRequest.until_date,
|
||||
)
|
||||
|
||||
|
||||
class TestChatMemberBannedWithoutRequest(ChatMemberTestBase):
|
||||
status = ChatMemberStatus.BANNED
|
||||
|
||||
def test_slot_behaviour(self, chat_member_banned):
|
||||
inst = chat_member_banned
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {
|
||||
"user": self.user.to_dict(),
|
||||
"until_date": to_timestamp(self.until_date),
|
||||
}
|
||||
chat_member = ChatMemberBanned.de_json(data, offline_bot)
|
||||
|
||||
assert type(chat_member) is ChatMemberBanned
|
||||
assert chat_member.api_kwargs == {}
|
||||
|
||||
assert chat_member.user == self.user
|
||||
assert chat_member.until_date == self.until_date
|
||||
|
||||
def test_de_json_localization(self, tz_bot, offline_bot, raw_bot):
|
||||
json_dict = {
|
||||
"user": self.user.to_dict(),
|
||||
"until_date": to_timestamp(self.until_date),
|
||||
}
|
||||
|
||||
cmb_raw = ChatMemberBanned.de_json(json_dict, raw_bot)
|
||||
cmb_bot = ChatMemberBanned.de_json(json_dict, offline_bot)
|
||||
cmb_bot_tz = ChatMemberBanned.de_json(json_dict, tz_bot)
|
||||
|
||||
# comparing utcoffsets because comparing timezones is unpredicatable
|
||||
cmb_bot_tz_offset = cmb_bot_tz.until_date.utcoffset()
|
||||
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(
|
||||
cmb_bot_tz.until_date.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
assert cmb_raw.until_date.tzinfo == UTC
|
||||
assert cmb_bot.until_date.tzinfo == UTC
|
||||
assert cmb_bot_tz_offset == tz_bot_offset
|
||||
|
||||
def test_to_dict(self, chat_member_banned):
|
||||
assert chat_member_banned.to_dict() == {
|
||||
"status": chat_member_banned.status,
|
||||
"user": chat_member_banned.user.to_dict(),
|
||||
"until_date": to_timestamp(chat_member_banned.until_date),
|
||||
}
|
||||
|
||||
def test_equality(self, chat_member_banned):
|
||||
a = chat_member_banned
|
||||
b = ChatMemberBanned(
|
||||
User(1, "test_user", is_bot=False), dtm.datetime.now(UTC).replace(microsecond=0)
|
||||
)
|
||||
c = ChatMemberBanned(
|
||||
User(2, "test_bot", is_bot=True), dtm.datetime.now(UTC).replace(microsecond=0)
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def chat_member_left():
|
||||
return ChatMemberLeft(TestChatMemberLeftWithoutRequest.user)
|
||||
|
||||
|
||||
class TestChatMemberLeftWithoutRequest(ChatMemberTestBase):
|
||||
status = ChatMemberStatus.LEFT
|
||||
|
||||
def test_slot_behaviour(self, chat_member_left):
|
||||
inst = chat_member_left
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"user": self.user.to_dict()}
|
||||
chat_member = ChatMemberLeft.de_json(data, offline_bot)
|
||||
|
||||
assert type(chat_member) is ChatMemberLeft
|
||||
assert chat_member.api_kwargs == {}
|
||||
|
||||
assert chat_member.user == self.user
|
||||
|
||||
def test_to_dict(self, chat_member_left):
|
||||
assert chat_member_left.to_dict() == {
|
||||
"status": chat_member_left.status,
|
||||
"user": chat_member_left.user.to_dict(),
|
||||
}
|
||||
|
||||
def test_equality(self, chat_member_left):
|
||||
a = chat_member_left
|
||||
b = ChatMemberLeft(User(1, "test_user", is_bot=False))
|
||||
c = ChatMemberLeft(User(2, "test_bot", is_bot=True))
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def chat_member_member():
|
||||
return ChatMemberMember(TestChatMemberMemberWithoutRequest.user)
|
||||
|
||||
|
||||
class TestChatMemberMemberWithoutRequest(ChatMemberTestBase):
|
||||
status = ChatMemberStatus.MEMBER
|
||||
|
||||
def test_slot_behaviour(self, chat_member_member):
|
||||
inst = chat_member_member
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"user": self.user.to_dict(), "until_date": to_timestamp(self.until_date)}
|
||||
chat_member = ChatMemberMember.de_json(data, offline_bot)
|
||||
|
||||
assert type(chat_member) is ChatMemberMember
|
||||
assert chat_member.api_kwargs == {}
|
||||
|
||||
assert chat_member.user == self.user
|
||||
assert chat_member.until_date == self.until_date
|
||||
|
||||
def test_de_json_localization(self, tz_bot, offline_bot, raw_bot):
|
||||
json_dict = {
|
||||
"user": self.user.to_dict(),
|
||||
"until_date": to_timestamp(self.until_date),
|
||||
}
|
||||
|
||||
cmm_raw = ChatMemberMember.de_json(json_dict, raw_bot)
|
||||
cmm_bot = ChatMemberMember.de_json(json_dict, offline_bot)
|
||||
cmm_bot_tz = ChatMemberMember.de_json(json_dict, tz_bot)
|
||||
|
||||
# comparing utcoffsets because comparing timezones is unpredicatable
|
||||
cmm_bot_tz_offset = cmm_bot_tz.until_date.utcoffset()
|
||||
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(
|
||||
cmm_bot_tz.until_date.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
assert cmm_raw.until_date.tzinfo == UTC
|
||||
assert cmm_bot.until_date.tzinfo == UTC
|
||||
assert cmm_bot_tz_offset == tz_bot_offset
|
||||
|
||||
def test_to_dict(self, chat_member_member):
|
||||
assert chat_member_member.to_dict() == {
|
||||
"status": chat_member_member.status,
|
||||
"user": chat_member_member.user.to_dict(),
|
||||
}
|
||||
|
||||
def test_equality(self, chat_member_member):
|
||||
a = chat_member_member
|
||||
b = ChatMemberMember(User(1, "test_user", is_bot=False))
|
||||
c = ChatMemberMember(User(2, "test_bot", is_bot=True))
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def chat_member_owner():
|
||||
return ChatMemberOwner(
|
||||
TestChatMemberOwnerWithoutRequest.user,
|
||||
TestChatMemberOwnerWithoutRequest.is_anonymous,
|
||||
TestChatMemberOwnerWithoutRequest.custom_title,
|
||||
)
|
||||
|
||||
|
||||
class TestChatMemberOwnerWithoutRequest(ChatMemberTestBase):
|
||||
status = ChatMemberStatus.OWNER
|
||||
|
||||
def test_slot_behaviour(self, chat_member_owner):
|
||||
inst = chat_member_owner
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {
|
||||
"user": self.user.to_dict(),
|
||||
"is_anonymous": self.is_anonymous,
|
||||
"custom_title": self.custom_title,
|
||||
}
|
||||
chat_member = ChatMemberOwner.de_json(data, offline_bot)
|
||||
|
||||
assert type(chat_member) is ChatMemberOwner
|
||||
assert chat_member.api_kwargs == {}
|
||||
|
||||
assert chat_member.user == self.user
|
||||
assert chat_member.is_anonymous == self.is_anonymous
|
||||
assert chat_member.custom_title == self.custom_title
|
||||
|
||||
def test_to_dict(self, chat_member_owner):
|
||||
assert chat_member_owner.to_dict() == {
|
||||
"status": chat_member_owner.status,
|
||||
"user": chat_member_owner.user.to_dict(),
|
||||
"is_anonymous": chat_member_owner.is_anonymous,
|
||||
"custom_title": chat_member_owner.custom_title,
|
||||
}
|
||||
|
||||
def test_equality(self, chat_member_owner):
|
||||
a = chat_member_owner
|
||||
b = ChatMemberOwner(User(1, "test_user", is_bot=False), True, "test_title")
|
||||
c = ChatMemberOwner(User(1, "test_user", is_bot=False), False, "test_title")
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def chat_member_restricted():
|
||||
return ChatMemberRestricted(
|
||||
user=TestChatMemberRestrictedWithoutRequest.user,
|
||||
can_add_web_page_previews=TestChatMemberRestrictedWithoutRequest.can_add_web_page_previews,
|
||||
can_change_info=TestChatMemberRestrictedWithoutRequest.can_change_info,
|
||||
can_invite_users=TestChatMemberRestrictedWithoutRequest.can_invite_users,
|
||||
can_manage_topics=TestChatMemberRestrictedWithoutRequest.can_manage_topics,
|
||||
can_pin_messages=TestChatMemberRestrictedWithoutRequest.can_pin_messages,
|
||||
can_send_audios=TestChatMemberRestrictedWithoutRequest.can_send_audios,
|
||||
can_send_documents=TestChatMemberRestrictedWithoutRequest.can_send_documents,
|
||||
can_send_messages=TestChatMemberRestrictedWithoutRequest.can_send_messages,
|
||||
can_send_other_messages=TestChatMemberRestrictedWithoutRequest.can_send_other_messages,
|
||||
can_send_photos=TestChatMemberRestrictedWithoutRequest.can_send_photos,
|
||||
can_send_polls=TestChatMemberRestrictedWithoutRequest.can_send_polls,
|
||||
can_send_video_notes=TestChatMemberRestrictedWithoutRequest.can_send_video_notes,
|
||||
can_send_videos=TestChatMemberRestrictedWithoutRequest.can_send_videos,
|
||||
can_send_voice_notes=TestChatMemberRestrictedWithoutRequest.can_send_voice_notes,
|
||||
is_member=TestChatMemberRestrictedWithoutRequest.is_member,
|
||||
until_date=TestChatMemberRestrictedWithoutRequest.until_date,
|
||||
)
|
||||
|
||||
|
||||
class TestChatMemberRestrictedWithoutRequest(ChatMemberTestBase):
|
||||
status = ChatMemberStatus.RESTRICTED
|
||||
|
||||
def test_slot_behaviour(self, chat_member_restricted):
|
||||
inst = chat_member_restricted
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {
|
||||
"user": self.user.to_dict(),
|
||||
"can_add_web_page_previews": self.can_add_web_page_previews,
|
||||
"can_change_info": self.can_change_info,
|
||||
"can_invite_users": self.can_invite_users,
|
||||
"can_manage_topics": self.can_manage_topics,
|
||||
"can_pin_messages": self.can_pin_messages,
|
||||
"can_send_audios": self.can_send_audios,
|
||||
"can_send_documents": self.can_send_documents,
|
||||
"can_send_messages": self.can_send_messages,
|
||||
"can_send_other_messages": self.can_send_other_messages,
|
||||
"can_send_photos": self.can_send_photos,
|
||||
"can_send_polls": self.can_send_polls,
|
||||
"can_send_video_notes": self.can_send_video_notes,
|
||||
"can_send_videos": self.can_send_videos,
|
||||
"can_send_voice_notes": self.can_send_voice_notes,
|
||||
"is_member": self.is_member,
|
||||
"until_date": to_timestamp(self.until_date),
|
||||
# legacy argument
|
||||
"can_send_media_messages": False,
|
||||
}
|
||||
chat_member = ChatMemberRestricted.de_json(data, offline_bot)
|
||||
|
||||
assert type(chat_member) is ChatMemberRestricted
|
||||
assert chat_member.api_kwargs == {"can_send_media_messages": False}
|
||||
|
||||
assert chat_member.user == self.user
|
||||
assert chat_member.can_add_web_page_previews == self.can_add_web_page_previews
|
||||
assert chat_member.can_change_info == self.can_change_info
|
||||
assert chat_member.can_invite_users == self.can_invite_users
|
||||
assert chat_member.can_manage_topics == self.can_manage_topics
|
||||
assert chat_member.can_pin_messages == self.can_pin_messages
|
||||
assert chat_member.can_send_audios == self.can_send_audios
|
||||
assert chat_member.can_send_documents == self.can_send_documents
|
||||
assert chat_member.can_send_messages == self.can_send_messages
|
||||
assert chat_member.can_send_other_messages == self.can_send_other_messages
|
||||
assert chat_member.can_send_photos == self.can_send_photos
|
||||
assert chat_member.can_send_polls == self.can_send_polls
|
||||
assert chat_member.can_send_video_notes == self.can_send_video_notes
|
||||
assert chat_member.can_send_videos == self.can_send_videos
|
||||
assert chat_member.can_send_voice_notes == self.can_send_voice_notes
|
||||
assert chat_member.is_member == self.is_member
|
||||
assert chat_member.until_date == self.until_date
|
||||
|
||||
def test_de_json_localization(self, tz_bot, offline_bot, raw_bot, chat_member_restricted):
|
||||
json_dict = chat_member_restricted.to_dict()
|
||||
|
||||
cmr_raw = ChatMemberRestricted.de_json(json_dict, raw_bot)
|
||||
cmr_bot = ChatMemberRestricted.de_json(json_dict, offline_bot)
|
||||
cmr_bot_tz = ChatMemberRestricted.de_json(json_dict, tz_bot)
|
||||
|
||||
# comparing utcoffsets because comparing timezones is unpredicatable
|
||||
cmr_bot_tz_offset = cmr_bot_tz.until_date.utcoffset()
|
||||
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(
|
||||
cmr_bot_tz.until_date.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
assert cmr_raw.until_date.tzinfo == UTC
|
||||
assert cmr_bot.until_date.tzinfo == UTC
|
||||
assert cmr_bot_tz_offset == tz_bot_offset
|
||||
|
||||
def test_to_dict(self, chat_member_restricted):
|
||||
assert chat_member_restricted.to_dict() == {
|
||||
"status": chat_member_restricted.status,
|
||||
"user": chat_member_restricted.user.to_dict(),
|
||||
"can_add_web_page_previews": chat_member_restricted.can_add_web_page_previews,
|
||||
"can_change_info": chat_member_restricted.can_change_info,
|
||||
"can_invite_users": chat_member_restricted.can_invite_users,
|
||||
"can_manage_topics": chat_member_restricted.can_manage_topics,
|
||||
"can_pin_messages": chat_member_restricted.can_pin_messages,
|
||||
"can_send_audios": chat_member_restricted.can_send_audios,
|
||||
"can_send_documents": chat_member_restricted.can_send_documents,
|
||||
"can_send_messages": chat_member_restricted.can_send_messages,
|
||||
"can_send_other_messages": chat_member_restricted.can_send_other_messages,
|
||||
"can_send_photos": chat_member_restricted.can_send_photos,
|
||||
"can_send_polls": chat_member_restricted.can_send_polls,
|
||||
"can_send_video_notes": chat_member_restricted.can_send_video_notes,
|
||||
"can_send_videos": chat_member_restricted.can_send_videos,
|
||||
"can_send_voice_notes": chat_member_restricted.can_send_voice_notes,
|
||||
"is_member": chat_member_restricted.is_member,
|
||||
"until_date": to_timestamp(chat_member_restricted.until_date),
|
||||
}
|
||||
|
||||
def test_equality(self, chat_member_restricted):
|
||||
a = chat_member_restricted
|
||||
b = deepcopy(chat_member_restricted)
|
||||
c = ChatMemberRestricted(
|
||||
User(1, "test_user", is_bot=False),
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
self.until_date,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a == c
|
||||
assert hash(a) == hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
@@ -164,43 +164,6 @@ class TestGiftWithoutRequest(GiftTestBase):
|
||||
pay_for_upgrade=True,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("id_name", ["user_id", "chat_id"])
|
||||
async def test_send_gift_user_chat_id(self, offline_bot, gift, monkeypatch, id_name):
|
||||
# Only here because we have to temporarily mark gift_id as optional.
|
||||
# tags: deprecated 21.11
|
||||
|
||||
# We can't send actual gifts, so we just check that the correct parameters are passed
|
||||
text_entities = [
|
||||
MessageEntity(MessageEntity.TEXT_LINK, 0, 4, "url"),
|
||||
MessageEntity(MessageEntity.BOLD, 5, 9),
|
||||
]
|
||||
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
received_id = request_data.parameters[id_name] == id_name
|
||||
gift_id = request_data.parameters["gift_id"] == "some_id"
|
||||
text = request_data.parameters["text"] == "text"
|
||||
text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode"
|
||||
tes = request_data.parameters["text_entities"] == [
|
||||
me.to_dict() for me in text_entities
|
||||
]
|
||||
pay_for_upgrade = request_data.parameters["pay_for_upgrade"] is True
|
||||
|
||||
return received_id and gift_id and text and text_parse_mode and tes and pay_for_upgrade
|
||||
|
||||
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
|
||||
assert await offline_bot.send_gift(
|
||||
gift_id=gift,
|
||||
text="text",
|
||||
text_parse_mode="text_parse_mode",
|
||||
text_entities=text_entities,
|
||||
pay_for_upgrade=True,
|
||||
**{id_name: id_name},
|
||||
)
|
||||
|
||||
async def test_send_gift_without_gift_id(self, offline_bot):
|
||||
with pytest.raises(TypeError, match="Missing required argument `gift_id`."):
|
||||
await offline_bot.send_gift()
|
||||
|
||||
@pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("passed_value", "expected_value"),
|
||||
|
||||
+136
-167
@@ -16,6 +16,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/].
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import (
|
||||
@@ -30,189 +32,134 @@ from telegram.constants import MenuButtonType
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def menu_button():
|
||||
return MenuButton(MenuButtonTestBase.type)
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
MenuButton.DEFAULT,
|
||||
MenuButton.WEB_APP,
|
||||
MenuButton.COMMANDS,
|
||||
],
|
||||
)
|
||||
def scope_type(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
MenuButtonDefault,
|
||||
MenuButtonCommands,
|
||||
MenuButtonWebApp,
|
||||
],
|
||||
ids=[
|
||||
MenuButton.DEFAULT,
|
||||
MenuButton.COMMANDS,
|
||||
MenuButton.WEB_APP,
|
||||
],
|
||||
)
|
||||
def scope_class(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
(MenuButtonDefault, MenuButton.DEFAULT),
|
||||
(MenuButtonCommands, MenuButton.COMMANDS),
|
||||
(MenuButtonWebApp, MenuButton.WEB_APP),
|
||||
],
|
||||
ids=[
|
||||
MenuButton.DEFAULT,
|
||||
MenuButton.COMMANDS,
|
||||
MenuButton.WEB_APP,
|
||||
],
|
||||
)
|
||||
def scope_class_and_type(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def menu_button(scope_class_and_type):
|
||||
# We use de_json here so that we don't have to worry about which class gets which arguments
|
||||
return scope_class_and_type[0].de_json(
|
||||
{
|
||||
"type": scope_class_and_type[1],
|
||||
"text": MenuButtonTestBase.text,
|
||||
"web_app": MenuButtonTestBase.web_app.to_dict(),
|
||||
},
|
||||
bot=None,
|
||||
)
|
||||
|
||||
|
||||
class MenuButtonTestBase:
|
||||
type = MenuButtonType.DEFAULT
|
||||
text = "this is a test string"
|
||||
web_app = WebAppInfo(url="https://python-telegram-bot.org")
|
||||
text = "button_text"
|
||||
web_app = WebAppInfo(url="https://python-telegram-bot.org/web_app")
|
||||
|
||||
|
||||
# All the scope types are very similar, so we test everything via parametrization
|
||||
class TestMenuButtonWithoutRequest(MenuButtonTestBase):
|
||||
def test_slot_behaviour(self, menu_button):
|
||||
inst = menu_button
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
for attr in menu_button.__slots__:
|
||||
assert getattr(menu_button, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(menu_button)) == len(set(mro_slots(menu_button))), "duplicate slot"
|
||||
|
||||
def test_type_enum_conversion(self, menu_button):
|
||||
assert type(MenuButton("default").type) is MenuButtonType
|
||||
assert MenuButton("unknown").type == "unknown"
|
||||
def test_de_json(self, offline_bot, scope_class_and_type):
|
||||
cls = scope_class_and_type[0]
|
||||
type_ = scope_class_and_type[1]
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"type": "unknown"}
|
||||
transaction_partner = MenuButton.de_json(data, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "unknown"
|
||||
json_dict = {"type": type_, "text": self.text, "web_app": self.web_app.to_dict()}
|
||||
menu_button = MenuButton.de_json(json_dict, offline_bot)
|
||||
assert set(menu_button.api_kwargs.keys()) == {"text", "web_app"} - set(cls.__slots__)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mb_type", "subclass"),
|
||||
[
|
||||
("commands", MenuButtonCommands),
|
||||
("web_app", MenuButtonWebApp),
|
||||
("default", MenuButtonDefault),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, mb_type, subclass):
|
||||
json_dict = {
|
||||
"type": mb_type,
|
||||
"web_app": self.web_app.to_dict(),
|
||||
"text": self.text,
|
||||
}
|
||||
mb = MenuButton.de_json(json_dict, offline_bot)
|
||||
assert isinstance(menu_button, MenuButton)
|
||||
assert type(menu_button) is cls
|
||||
assert menu_button.type == type_
|
||||
if "web_app" in cls.__slots__:
|
||||
assert menu_button.web_app == self.web_app
|
||||
if "text" in cls.__slots__:
|
||||
assert menu_button.text == self.text
|
||||
|
||||
assert type(mb) is subclass
|
||||
assert set(mb.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
|
||||
"type"
|
||||
}
|
||||
assert mb.type == mb_type
|
||||
def test_de_json_invalid_type(self, offline_bot):
|
||||
json_dict = {"type": "invalid", "text": self.text, "web_app": self.web_app.to_dict()}
|
||||
menu_button = MenuButton.de_json(json_dict, offline_bot)
|
||||
assert menu_button.api_kwargs == {"text": self.text, "web_app": self.web_app.to_dict()}
|
||||
|
||||
assert type(menu_button) is MenuButton
|
||||
assert menu_button.type == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, scope_class, offline_bot):
|
||||
"""This makes sure that e.g. MenuButtonDefault(data) never returns a
|
||||
MenuButtonChat instance."""
|
||||
json_dict = {"type": "invalid", "text": self.text, "web_app": self.web_app.to_dict()}
|
||||
assert type(scope_class.de_json(json_dict, offline_bot)) is scope_class
|
||||
|
||||
def test_de_json_empty_data(self, scope_class):
|
||||
if scope_class in (MenuButtonWebApp,):
|
||||
pytest.skip(
|
||||
"This test is not relevant for subclasses that have more attributes than just type"
|
||||
)
|
||||
assert isinstance(scope_class.de_json({}, None), scope_class)
|
||||
|
||||
def test_to_dict(self, menu_button):
|
||||
assert menu_button.to_dict() == {"type": menu_button.type}
|
||||
menu_button_dict = menu_button.to_dict()
|
||||
|
||||
def test_equality(self, menu_button):
|
||||
a = menu_button
|
||||
b = MenuButton(self.type)
|
||||
c = MenuButton("unknown")
|
||||
d = Dice(5, "test")
|
||||
assert isinstance(menu_button_dict, dict)
|
||||
assert menu_button_dict["type"] == menu_button.type
|
||||
if hasattr(menu_button, "web_app"):
|
||||
assert menu_button_dict["web_app"] == menu_button.web_app.to_dict()
|
||||
if hasattr(menu_button, "text"):
|
||||
assert menu_button_dict["text"] == menu_button.text
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
def test_type_enum_conversion(self):
|
||||
assert type(MenuButton("commands").type) is MenuButtonType
|
||||
assert MenuButton("unknown").type == "unknown"
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def menu_button_commands():
|
||||
return MenuButtonCommands()
|
||||
|
||||
|
||||
class TestMenuButtonCommandsWithoutRequest(MenuButtonTestBase):
|
||||
type = MenuButtonType.COMMANDS
|
||||
|
||||
def test_slot_behaviour(self, menu_button_commands):
|
||||
inst = menu_button_commands
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = MenuButtonCommands.de_json({}, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "commands"
|
||||
|
||||
def test_to_dict(self, menu_button_commands):
|
||||
assert menu_button_commands.to_dict() == {"type": menu_button_commands.type}
|
||||
|
||||
def test_equality(self, menu_button_commands):
|
||||
a = menu_button_commands
|
||||
b = MenuButtonCommands()
|
||||
c = Dice(5, "test")
|
||||
d = MenuButtonDefault()
|
||||
|
||||
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
|
||||
def menu_button_default():
|
||||
return MenuButtonDefault()
|
||||
|
||||
|
||||
class TestMenuButtonDefaultWithoutRequest(MenuButtonTestBase):
|
||||
type = MenuButtonType.DEFAULT
|
||||
|
||||
def test_slot_behaviour(self, menu_button_default):
|
||||
inst = menu_button_default
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
transaction_partner = MenuButtonDefault.de_json({}, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "default"
|
||||
|
||||
def test_to_dict(self, menu_button_default):
|
||||
assert menu_button_default.to_dict() == {"type": menu_button_default.type}
|
||||
|
||||
def test_equality(self, menu_button_default):
|
||||
a = menu_button_default
|
||||
b = MenuButtonDefault()
|
||||
c = Dice(5, "test")
|
||||
d = MenuButtonCommands()
|
||||
|
||||
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
|
||||
def menu_button_web_app():
|
||||
return MenuButtonWebApp(
|
||||
web_app=TestMenuButtonWebAppWithoutRequest.web_app,
|
||||
text=TestMenuButtonWebAppWithoutRequest.text,
|
||||
)
|
||||
|
||||
|
||||
class TestMenuButtonWebAppWithoutRequest(MenuButtonTestBase):
|
||||
type = MenuButtonType.WEB_APP
|
||||
|
||||
def test_slot_behaviour(self, menu_button_web_app):
|
||||
inst = menu_button_web_app
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {"web_app": self.web_app.to_dict(), "text": self.text}
|
||||
transaction_partner = MenuButtonWebApp.de_json(json_dict, offline_bot)
|
||||
assert transaction_partner.api_kwargs == {}
|
||||
assert transaction_partner.type == "web_app"
|
||||
assert transaction_partner.web_app == self.web_app
|
||||
assert transaction_partner.text == self.text
|
||||
|
||||
def test_to_dict(self, menu_button_web_app):
|
||||
assert menu_button_web_app.to_dict() == {
|
||||
"type": menu_button_web_app.type,
|
||||
"web_app": menu_button_web_app.web_app.to_dict(),
|
||||
"text": menu_button_web_app.text,
|
||||
}
|
||||
|
||||
def test_equality(self, menu_button_web_app):
|
||||
a = menu_button_web_app
|
||||
b = MenuButtonWebApp(web_app=self.web_app, text=self.text)
|
||||
c = MenuButtonWebApp(web_app=self.web_app, text="other text")
|
||||
d = MenuButtonWebApp(web_app=WebAppInfo(url="https://example.org"), text=self.text)
|
||||
e = Dice(5, "test")
|
||||
def test_equality(self, menu_button, offline_bot):
|
||||
a = MenuButton("base_type")
|
||||
b = MenuButton("base_type")
|
||||
c = menu_button
|
||||
d = deepcopy(menu_button)
|
||||
e = Dice(4, "emoji")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -225,3 +172,25 @@ class TestMenuButtonWebAppWithoutRequest(MenuButtonTestBase):
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
if hasattr(c, "web_app"):
|
||||
json_dict = c.to_dict()
|
||||
json_dict["web_app"] = WebAppInfo("https://foo.bar/web_app").to_dict()
|
||||
f = c.__class__.de_json(json_dict, offline_bot)
|
||||
|
||||
assert c != f
|
||||
assert hash(c) != hash(f)
|
||||
|
||||
if hasattr(c, "text"):
|
||||
json_dict = c.to_dict()
|
||||
json_dict["text"] = "other text"
|
||||
g = c.__class__.de_json(json_dict, offline_bot)
|
||||
|
||||
assert c != g
|
||||
assert hash(c) != hash(g)
|
||||
|
||||
@@ -2216,9 +2216,9 @@ class TestMessageWithoutRequest(MessageTestBase):
|
||||
"title",
|
||||
"description",
|
||||
"payload",
|
||||
"provider_token",
|
||||
"currency",
|
||||
"prices",
|
||||
"provider_token",
|
||||
)
|
||||
await self.check_quote_parsing(
|
||||
message,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"""This module contains exceptions to our API compared to the official API."""
|
||||
import datetime as dtm
|
||||
|
||||
from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice
|
||||
from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice
|
||||
from tests.test_official.helpers import _get_params_base
|
||||
|
||||
IGNORED_OBJECTS = ("ResponseParameters",)
|
||||
@@ -47,8 +47,7 @@ class ParamTypeCheckingExceptions:
|
||||
"animation": Animation,
|
||||
"voice": Voice,
|
||||
"sticker": Sticker,
|
||||
# TODO: Deprecated and will be corrected (and readded) in next major bot API release:
|
||||
# "gift_id": Gift,
|
||||
"gift_id": Gift,
|
||||
},
|
||||
"(delete|set)_sticker.*": {
|
||||
"sticker$": Sticker,
|
||||
@@ -73,40 +72,36 @@ class ParamTypeCheckingExceptions:
|
||||
("keyboard", True): "KeyboardButton", # + sequence[sequence[str]]
|
||||
("reaction", False): "ReactionType", # + str
|
||||
("options", False): "InputPollOption", # + str
|
||||
# TODO: Deprecated and will be corrected (and removed) in next bot api release
|
||||
# TODO: Deprecated and will be corrected (and removed) in next major PTB version:
|
||||
("file_hashes", True): "list[str]",
|
||||
}
|
||||
|
||||
# Special cases for other parameters that accept more types than the official API, and are
|
||||
# too complex to compare/predict with official API
|
||||
# structure: class/method_name: {param_name: reduced form of annotation}
|
||||
COMPLEX_TYPES = {
|
||||
"send_poll": {"correct_option_id": int}, # actual: Literal
|
||||
"get_file": {
|
||||
"file_id": str, # actual: Union[str, objs_with_file_id_attr]
|
||||
},
|
||||
r"\w+invite_link": {
|
||||
"invite_link": str, # actual: Union[str, ChatInviteLink]
|
||||
},
|
||||
"send_invoice|create_invoice_link": {
|
||||
"provider_data": str, # actual: Union[str, obj]
|
||||
},
|
||||
"InlineKeyboardButton": {
|
||||
"callback_data": str, # actual: Union[str, obj]
|
||||
},
|
||||
"Input(Paid)?Media.*": {
|
||||
"media": str, # actual: Union[str, InputMedia*, FileInput]
|
||||
# see also https://github.com/tdlib/telegram-bot-api/issues/707
|
||||
"thumbnail": str, # actual: Union[str, FileInput]
|
||||
"cover": str, # actual: Union[str, FileInput]
|
||||
},
|
||||
"EncryptedPassportElement": {
|
||||
"data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
|
||||
},
|
||||
# TODO: Deprecated and will be corrected (and removed) in next major PTB
|
||||
# version:
|
||||
"send_gift": {"gift_id": str}, # actual: Non optional
|
||||
}
|
||||
COMPLEX_TYPES = (
|
||||
{ # (param_name, is_class (i.e appears in a class?)): reduced form of annotation
|
||||
"send_poll": {"correct_option_id": int}, # actual: Literal
|
||||
"get_file": {
|
||||
"file_id": str, # actual: Union[str, objs_with_file_id_attr]
|
||||
},
|
||||
r"\w+invite_link": {
|
||||
"invite_link": str, # actual: Union[str, ChatInviteLink]
|
||||
},
|
||||
"send_invoice|create_invoice_link": {
|
||||
"provider_data": str, # actual: Union[str, obj]
|
||||
},
|
||||
"InlineKeyboardButton": {
|
||||
"callback_data": str, # actual: Union[str, obj]
|
||||
},
|
||||
"Input(Paid)?Media.*": {
|
||||
"media": str, # actual: Union[str, InputMedia*, FileInput]
|
||||
},
|
||||
"EncryptedPassportElement": {
|
||||
"data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# param names ignored in the param type checking in classes for the `tg.Defaults` case.
|
||||
IGNORED_DEFAULTS_PARAM_NAMES = {
|
||||
@@ -203,8 +198,6 @@ IGNORED_PARAM_REQUIREMENTS = {
|
||||
"send_venue": {"latitude", "longitude", "title", "address"},
|
||||
"send_contact": {"phone_number", "first_name"},
|
||||
# ---->
|
||||
# here for backwards compatibility. Todo: remove on next bot api release
|
||||
"send_gift": {"gift_id"},
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +206,10 @@ def ignored_param_requirements(object_name: str) -> set[str]:
|
||||
|
||||
|
||||
# Arguments that are optional arguments for now for backwards compatibility
|
||||
BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {}
|
||||
BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {
|
||||
"send_invoice|create_invoice_link|InputInvoiceMessageContent": {"provider_token"},
|
||||
"InlineQueryResultArticle": {"hide_url"},
|
||||
}
|
||||
|
||||
|
||||
def backwards_compat_kwargs(object_name: str) -> set[str]:
|
||||
|
||||
+198
-238
@@ -37,13 +37,102 @@ from telegram.constants import PaidMediaType
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def paid_media():
|
||||
return PaidMedia(type=PaidMediaType.PHOTO)
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
PaidMedia.PREVIEW,
|
||||
PaidMedia.PHOTO,
|
||||
PaidMedia.VIDEO,
|
||||
],
|
||||
)
|
||||
def pm_scope_type(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
PaidMediaPreview,
|
||||
PaidMediaPhoto,
|
||||
PaidMediaVideo,
|
||||
],
|
||||
ids=[
|
||||
PaidMedia.PREVIEW,
|
||||
PaidMedia.PHOTO,
|
||||
PaidMedia.VIDEO,
|
||||
],
|
||||
)
|
||||
def pm_scope_class(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
(
|
||||
PaidMediaPreview,
|
||||
PaidMedia.PREVIEW,
|
||||
),
|
||||
(
|
||||
PaidMediaPhoto,
|
||||
PaidMedia.PHOTO,
|
||||
),
|
||||
(
|
||||
PaidMediaVideo,
|
||||
PaidMedia.VIDEO,
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
PaidMedia.PREVIEW,
|
||||
PaidMedia.PHOTO,
|
||||
PaidMedia.VIDEO,
|
||||
],
|
||||
)
|
||||
def pm_scope_class_and_type(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def paid_media(pm_scope_class_and_type):
|
||||
# We use de_json here so that we don't have to worry about which class gets which arguments
|
||||
return pm_scope_class_and_type[0].de_json(
|
||||
{
|
||||
"type": pm_scope_class_and_type[1],
|
||||
"width": PaidMediaTestBase.width,
|
||||
"height": PaidMediaTestBase.height,
|
||||
"duration": PaidMediaTestBase.duration,
|
||||
"video": PaidMediaTestBase.video.to_dict(),
|
||||
"photo": [p.to_dict() for p in PaidMediaTestBase.photo],
|
||||
},
|
||||
bot=None,
|
||||
)
|
||||
|
||||
|
||||
def paid_media_video():
|
||||
return PaidMediaVideo(video=PaidMediaTestBase.video)
|
||||
|
||||
|
||||
def paid_media_photo():
|
||||
return PaidMediaPhoto(photo=PaidMediaTestBase.photo)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def paid_media_info():
|
||||
return PaidMediaInfo(
|
||||
star_count=PaidMediaInfoTestBase.star_count,
|
||||
paid_media=[paid_media_video(), paid_media_photo()],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def paid_media_purchased():
|
||||
return PaidMediaPurchased(
|
||||
from_user=PaidMediaPurchasedTestBase.from_user,
|
||||
paid_media_payload=PaidMediaPurchasedTestBase.paid_media_payload,
|
||||
)
|
||||
|
||||
|
||||
class PaidMediaTestBase:
|
||||
type = PaidMediaType.PHOTO
|
||||
width = 640
|
||||
height = 480
|
||||
duration = 60
|
||||
@@ -71,49 +160,97 @@ class TestPaidMediaWithoutRequest(PaidMediaTestBase):
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_type_enum_conversion(self, paid_media):
|
||||
assert type(PaidMedia("photo").type) is PaidMediaType
|
||||
assert PaidMedia("unknown").type == "unknown"
|
||||
def test_de_json(self, offline_bot, pm_scope_class_and_type):
|
||||
cls = pm_scope_class_and_type[0]
|
||||
type_ = pm_scope_class_and_type[1]
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
data = {"type": "unknown"}
|
||||
paid_media = PaidMedia.de_json(data, offline_bot)
|
||||
assert paid_media.api_kwargs == {}
|
||||
assert paid_media.type == "unknown"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("pm_type", "subclass"),
|
||||
[
|
||||
("photo", PaidMediaPhoto),
|
||||
("video", PaidMediaVideo),
|
||||
("preview", PaidMediaPreview),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, pm_type, subclass):
|
||||
json_dict = {
|
||||
"type": pm_type,
|
||||
"video": self.video.to_dict(),
|
||||
"photo": [p.to_dict() for p in self.photo],
|
||||
"type": type_,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"duration": self.duration,
|
||||
"video": self.video.to_dict(),
|
||||
"photo": [p.to_dict() for p in self.photo],
|
||||
}
|
||||
pm = PaidMedia.de_json(json_dict, offline_bot)
|
||||
assert set(pm.api_kwargs.keys()) == {
|
||||
"width",
|
||||
"height",
|
||||
"duration",
|
||||
"video",
|
||||
"photo",
|
||||
} - set(cls.__slots__)
|
||||
|
||||
assert type(pm) is subclass
|
||||
assert set(pm.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
|
||||
"type"
|
||||
assert isinstance(pm, PaidMedia)
|
||||
assert type(pm) is cls
|
||||
assert pm.type == type_
|
||||
if "width" in cls.__slots__:
|
||||
assert pm.width == self.width
|
||||
assert pm.height == self.height
|
||||
assert pm.duration == self.duration
|
||||
if "video" in cls.__slots__:
|
||||
assert pm.video == self.video
|
||||
if "photo" in cls.__slots__:
|
||||
assert pm.photo == self.photo
|
||||
|
||||
def test_de_json_invalid_type(self, offline_bot):
|
||||
json_dict = {
|
||||
"type": "invalid",
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"duration": self.duration,
|
||||
"video": self.video.to_dict(),
|
||||
"photo": [p.to_dict() for p in self.photo],
|
||||
}
|
||||
assert pm.type == pm_type
|
||||
pm = PaidMedia.de_json(json_dict, offline_bot)
|
||||
assert pm.api_kwargs == {
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"duration": self.duration,
|
||||
"video": self.video.to_dict(),
|
||||
"photo": [p.to_dict() for p in self.photo],
|
||||
}
|
||||
|
||||
assert type(pm) is PaidMedia
|
||||
assert pm.type == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, pm_scope_class, offline_bot):
|
||||
"""This makes sure that e.g. PaidMediaPreivew(data) never returns a
|
||||
TransactionPartnerPhoto instance."""
|
||||
json_dict = {
|
||||
"type": "invalid",
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"duration": self.duration,
|
||||
"video": self.video.to_dict(),
|
||||
"photo": [p.to_dict() for p in self.photo],
|
||||
}
|
||||
assert type(pm_scope_class.de_json(json_dict, offline_bot)) is pm_scope_class
|
||||
|
||||
def test_to_dict(self, paid_media):
|
||||
assert paid_media.to_dict() == {"type": paid_media.type}
|
||||
pm_dict = paid_media.to_dict()
|
||||
|
||||
def test_equality(self, paid_media):
|
||||
a = paid_media
|
||||
b = PaidMedia(self.type)
|
||||
c = PaidMedia("unknown")
|
||||
d = Dice(5, "test")
|
||||
assert isinstance(pm_dict, dict)
|
||||
assert pm_dict["type"] == paid_media.type
|
||||
if hasattr(paid_media_info, "width"):
|
||||
assert pm_dict["width"] == paid_media.width
|
||||
assert pm_dict["height"] == paid_media.height
|
||||
assert pm_dict["duration"] == paid_media.duration
|
||||
if hasattr(paid_media_info, "video"):
|
||||
assert pm_dict["video"] == paid_media.video.to_dict()
|
||||
if hasattr(paid_media_info, "photo"):
|
||||
assert pm_dict["photo"] == [p.to_dict() for p in paid_media.photo]
|
||||
|
||||
def test_type_enum_conversion(self):
|
||||
assert type(PaidMedia("video").type) is PaidMediaType
|
||||
assert PaidMedia("unknown").type == "unknown"
|
||||
|
||||
def test_equality(self, paid_media, offline_bot):
|
||||
a = PaidMedia("base_type")
|
||||
b = PaidMedia("base_type")
|
||||
c = paid_media
|
||||
d = deepcopy(paid_media)
|
||||
e = Dice(4, "emoji")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -124,216 +261,35 @@ class TestPaidMediaWithoutRequest(PaidMediaTestBase):
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
@pytest.fixture
|
||||
def paid_media_photo():
|
||||
return PaidMediaPhoto(
|
||||
photo=TestPaidMediaPhotoWithoutRequest.photo,
|
||||
)
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
class TestPaidMediaPhotoWithoutRequest(PaidMediaTestBase):
|
||||
type = PaidMediaType.PHOTO
|
||||
if hasattr(c, "video"):
|
||||
json_dict = c.to_dict()
|
||||
json_dict["video"] = Video("different", "d2", 1, 1, 1).to_dict()
|
||||
f = c.__class__.de_json(json_dict, offline_bot)
|
||||
|
||||
def test_slot_behaviour(self, paid_media_photo):
|
||||
inst = paid_media_photo
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
assert c != f
|
||||
assert hash(c) != hash(f)
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"photo": [p.to_dict() for p in self.photo],
|
||||
}
|
||||
pmp = PaidMediaPhoto.de_json(json_dict, offline_bot)
|
||||
assert pmp.photo == tuple(self.photo)
|
||||
assert pmp.api_kwargs == {}
|
||||
if hasattr(c, "photo"):
|
||||
json_dict = c.to_dict()
|
||||
json_dict["photo"] = [PhotoSize("different", "d2", 1, 1, 1).to_dict()]
|
||||
f = c.__class__.de_json(json_dict, offline_bot)
|
||||
|
||||
def test_to_dict(self, paid_media_photo):
|
||||
assert paid_media_photo.to_dict() == {
|
||||
"type": paid_media_photo.type,
|
||||
"photo": [p.to_dict() for p in self.photo],
|
||||
}
|
||||
|
||||
def test_equality(self, paid_media_photo):
|
||||
a = paid_media_photo
|
||||
b = PaidMediaPhoto(deepcopy(self.photo))
|
||||
c = PaidMediaPhoto([PhotoSize("file_id", 640, 480, "file_unique_id")])
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def paid_media_video():
|
||||
return PaidMediaVideo(
|
||||
video=TestPaidMediaVideoWithoutRequest.video,
|
||||
)
|
||||
|
||||
|
||||
class TestPaidMediaVideoWithoutRequest(PaidMediaTestBase):
|
||||
type = PaidMediaType.VIDEO
|
||||
|
||||
def test_slot_behaviour(self, paid_media_video):
|
||||
inst = paid_media_video
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"video": self.video.to_dict(),
|
||||
}
|
||||
pmv = PaidMediaVideo.de_json(json_dict, offline_bot)
|
||||
assert pmv.video == self.video
|
||||
assert pmv.api_kwargs == {}
|
||||
|
||||
def test_to_dict(self, paid_media_video):
|
||||
assert paid_media_video.to_dict() == {
|
||||
"type": self.type,
|
||||
"video": paid_media_video.video.to_dict(),
|
||||
}
|
||||
|
||||
def test_equality(self, paid_media_video):
|
||||
a = paid_media_video
|
||||
b = PaidMediaVideo(
|
||||
video=deepcopy(self.video),
|
||||
)
|
||||
c = PaidMediaVideo(
|
||||
video=Video("test", "test_unique", 640, 480, 60),
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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
|
||||
def paid_media_preview():
|
||||
return PaidMediaPreview(
|
||||
width=TestPaidMediaPreviewWithoutRequest.width,
|
||||
height=TestPaidMediaPreviewWithoutRequest.height,
|
||||
duration=TestPaidMediaPreviewWithoutRequest.duration,
|
||||
)
|
||||
|
||||
|
||||
class TestPaidMediaPreviewWithoutRequest(PaidMediaTestBase):
|
||||
type = PaidMediaType.PREVIEW
|
||||
|
||||
def test_slot_behaviour(self, paid_media_preview):
|
||||
inst = paid_media_preview
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"duration": self.duration,
|
||||
}
|
||||
pmp = PaidMediaPreview.de_json(json_dict, offline_bot)
|
||||
assert pmp.width == self.width
|
||||
assert pmp.height == self.height
|
||||
assert pmp.duration == self.duration
|
||||
assert pmp.api_kwargs == {}
|
||||
|
||||
def test_to_dict(self, paid_media_preview):
|
||||
assert paid_media_preview.to_dict() == {
|
||||
"type": paid_media_preview.type,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"duration": self.duration,
|
||||
}
|
||||
|
||||
def test_equality(self, paid_media_preview):
|
||||
a = paid_media_preview
|
||||
b = PaidMediaPreview(
|
||||
width=self.width,
|
||||
height=self.height,
|
||||
duration=self.duration,
|
||||
)
|
||||
c = PaidMediaPreview(
|
||||
width=100,
|
||||
height=100,
|
||||
duration=100,
|
||||
)
|
||||
d = Dice(5, "test")
|
||||
|
||||
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 paid_media_info():
|
||||
return PaidMediaInfo(
|
||||
star_count=PaidMediaInfoTestBase.star_count,
|
||||
paid_media=PaidMediaInfoTestBase.paid_media,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def paid_media_purchased():
|
||||
return PaidMediaPurchased(
|
||||
from_user=PaidMediaPurchasedTestBase.from_user,
|
||||
paid_media_payload=PaidMediaPurchasedTestBase.paid_media_payload,
|
||||
)
|
||||
assert c != f
|
||||
assert hash(c) != hash(f)
|
||||
|
||||
|
||||
class PaidMediaInfoTestBase:
|
||||
star_count = 200
|
||||
paid_media = [
|
||||
PaidMediaVideo(
|
||||
video=Video(
|
||||
file_id="video_file_id",
|
||||
width=640,
|
||||
height=480,
|
||||
file_unique_id="file_unique_id",
|
||||
duration=60,
|
||||
)
|
||||
),
|
||||
PaidMediaPhoto(
|
||||
photo=[
|
||||
PhotoSize(
|
||||
file_id="photo_file_id",
|
||||
width=640,
|
||||
height=480,
|
||||
file_unique_id="file_unique_id",
|
||||
)
|
||||
]
|
||||
),
|
||||
]
|
||||
paid_media = [paid_media_video(), paid_media_photo()]
|
||||
|
||||
|
||||
class TestPaidMediaInfoWithoutRequest(PaidMediaInfoTestBase):
|
||||
@@ -359,9 +315,13 @@ class TestPaidMediaInfoWithoutRequest(PaidMediaInfoTestBase):
|
||||
}
|
||||
|
||||
def test_equality(self):
|
||||
pmi1 = PaidMediaInfo(star_count=self.star_count, paid_media=self.paid_media)
|
||||
pmi2 = PaidMediaInfo(star_count=self.star_count, paid_media=self.paid_media)
|
||||
pmi3 = PaidMediaInfo(star_count=100, paid_media=[self.paid_media[0]])
|
||||
pmi1 = PaidMediaInfo(
|
||||
star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()]
|
||||
)
|
||||
pmi2 = PaidMediaInfo(
|
||||
star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()]
|
||||
)
|
||||
pmi3 = PaidMediaInfo(star_count=100, paid_media=[paid_media_photo()])
|
||||
|
||||
assert pmi1 == pmi2
|
||||
assert hash(pmi1) == hash(pmi2)
|
||||
|
||||
+147
-137
@@ -16,6 +16,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 inspect
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -27,100 +29,161 @@ from telegram import (
|
||||
ReactionTypeCustomEmoji,
|
||||
ReactionTypeEmoji,
|
||||
ReactionTypePaid,
|
||||
constants,
|
||||
)
|
||||
from telegram.constants import ReactionEmoji
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reaction_type():
|
||||
return ReactionType(type=TestReactionTypeWithoutRequest.type)
|
||||
ignored = ["self", "api_kwargs"]
|
||||
|
||||
|
||||
class ReactionTypeTestBase:
|
||||
type = "emoji"
|
||||
emoji = "some_emoji"
|
||||
custom_emoji_id = "some_custom_emoji_id"
|
||||
class RTDefaults:
|
||||
custom_emoji = "123custom"
|
||||
normal_emoji = ReactionEmoji.THUMBS_UP
|
||||
|
||||
|
||||
class TestReactionTypeWithoutRequest(ReactionTypeTestBase):
|
||||
def reaction_type_custom_emoji():
|
||||
return ReactionTypeCustomEmoji(RTDefaults.custom_emoji)
|
||||
|
||||
|
||||
def reaction_type_emoji():
|
||||
return ReactionTypeEmoji(RTDefaults.normal_emoji)
|
||||
|
||||
|
||||
def reaction_type_paid():
|
||||
return ReactionTypePaid()
|
||||
|
||||
|
||||
def make_json_dict(instance: ReactionType, include_optional_args: bool = False) -> dict:
|
||||
"""Used to make the json dict which we use for testing de_json. Similar to iter_args()"""
|
||||
json_dict = {"type": instance.type}
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored: # ignore irrelevant params
|
||||
continue
|
||||
|
||||
val = getattr(instance, param.name)
|
||||
# Compulsory args-
|
||||
if param.default is inspect.Parameter.empty:
|
||||
if hasattr(val, "to_dict"): # convert the user object or any future ones to dict.
|
||||
val = val.to_dict()
|
||||
json_dict[param.name] = val
|
||||
|
||||
# If we want to test all args (for de_json)-
|
||||
# currently not needed, keeping for completeness
|
||||
elif param.default is not inspect.Parameter.empty and include_optional_args:
|
||||
json_dict[param.name] = val
|
||||
return json_dict
|
||||
|
||||
|
||||
def iter_args(instance: ReactionType, de_json_inst: ReactionType, include_optional: bool = False):
|
||||
"""
|
||||
We accept both the regular instance and de_json created instance and iterate over them for
|
||||
easy one line testing later one.
|
||||
"""
|
||||
yield instance.type, de_json_inst.type # yield this here cause it's not available in sig.
|
||||
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored:
|
||||
continue
|
||||
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
|
||||
if (
|
||||
param.default is not inspect.Parameter.empty and include_optional
|
||||
) or param.default is inspect.Parameter.empty:
|
||||
yield inst_at, json_at
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reaction_type(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reaction_type",
|
||||
[
|
||||
reaction_type_custom_emoji,
|
||||
reaction_type_emoji,
|
||||
reaction_type_paid,
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
class TestReactionTypesWithoutRequest:
|
||||
def test_slot_behaviour(self, reaction_type):
|
||||
inst = reaction_type
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_type_enum_conversion(self):
|
||||
assert type(ReactionType("emoji").type) is constants.ReactionType
|
||||
assert ReactionType("unknown").type == "unknown"
|
||||
def test_de_json_required_args(self, offline_bot, reaction_type):
|
||||
cls = reaction_type.__class__
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {"type": "unknown"}
|
||||
json_dict = make_json_dict(reaction_type)
|
||||
const_reaction_type = ReactionType.de_json(json_dict, offline_bot)
|
||||
assert const_reaction_type.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_reaction_type, ReactionType)
|
||||
assert isinstance(const_reaction_type, cls)
|
||||
for reaction_type_at, const_reaction_type_at in iter_args(
|
||||
reaction_type, const_reaction_type
|
||||
):
|
||||
assert reaction_type_at == const_reaction_type_at
|
||||
|
||||
def test_de_json_all_args(self, offline_bot, reaction_type):
|
||||
json_dict = make_json_dict(reaction_type, include_optional_args=True)
|
||||
const_reaction_type = ReactionType.de_json(json_dict, offline_bot)
|
||||
assert const_reaction_type.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_reaction_type, ReactionType)
|
||||
assert isinstance(const_reaction_type, reaction_type.__class__)
|
||||
for c_mem_type_at, const_c_mem_at in iter_args(reaction_type, const_reaction_type, True):
|
||||
assert c_mem_type_at == const_c_mem_at
|
||||
|
||||
def test_de_json_invalid_type(self, offline_bot, reaction_type):
|
||||
json_dict = {"type": "invalid"}
|
||||
reaction_type = ReactionType.de_json(json_dict, offline_bot)
|
||||
assert reaction_type.api_kwargs == {}
|
||||
assert reaction_type.type == "unknown"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rt_type", "subclass"),
|
||||
[
|
||||
("emoji", ReactionTypeEmoji),
|
||||
("custom_emoji", ReactionTypeCustomEmoji),
|
||||
("paid", ReactionTypePaid),
|
||||
],
|
||||
)
|
||||
def test_de_json_subclass(self, offline_bot, rt_type, subclass):
|
||||
json_dict = {
|
||||
"type": rt_type,
|
||||
"emoji": self.emoji,
|
||||
"custom_emoji_id": self.custom_emoji_id,
|
||||
}
|
||||
rt = ReactionType.de_json(json_dict, offline_bot)
|
||||
assert type(reaction_type) is ReactionType
|
||||
assert reaction_type.type == "invalid"
|
||||
|
||||
assert type(rt) is subclass
|
||||
assert set(rt.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
|
||||
"type"
|
||||
}
|
||||
assert rt.type == rt_type
|
||||
def test_de_json_subclass(self, reaction_type, offline_bot, chat_id):
|
||||
"""This makes sure that e.g. ReactionTypeEmoji(data, offline_bot) never returns a
|
||||
ReactionTypeCustomEmoji instance."""
|
||||
cls = reaction_type.__class__
|
||||
json_dict = make_json_dict(reaction_type, True)
|
||||
assert type(cls.de_json(json_dict, offline_bot)) is cls
|
||||
|
||||
def test_to_dict(self, reaction_type):
|
||||
reaction_type_dict = reaction_type.to_dict()
|
||||
|
||||
assert isinstance(reaction_type_dict, dict)
|
||||
assert reaction_type_dict["type"] == reaction_type.type
|
||||
if reaction_type.type == ReactionType.EMOJI:
|
||||
assert reaction_type_dict["emoji"] == reaction_type.emoji
|
||||
elif reaction_type.type == ReactionType.CUSTOM_EMOJI:
|
||||
assert reaction_type_dict["custom_emoji_id"] == reaction_type.custom_emoji_id
|
||||
|
||||
for slot in reaction_type.__slots__: # additional verification for the optional args
|
||||
assert getattr(reaction_type, slot) == reaction_type_dict[slot]
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reaction_type_emoji():
|
||||
return ReactionTypeEmoji(emoji=TestReactionTypeEmojiWithoutRequest.emoji)
|
||||
def test_reaction_type_api_kwargs(self, reaction_type):
|
||||
json_dict = make_json_dict(reaction_type_custom_emoji())
|
||||
json_dict["custom_arg"] = "wuhu"
|
||||
reaction_type_custom_emoji_instance = ReactionType.de_json(json_dict, None)
|
||||
assert reaction_type_custom_emoji_instance.api_kwargs == {
|
||||
"custom_arg": "wuhu",
|
||||
}
|
||||
|
||||
|
||||
class TestReactionTypeEmojiWithoutRequest(ReactionTypeTestBase):
|
||||
type = constants.ReactionType.EMOJI
|
||||
|
||||
def test_slot_behaviour(self, reaction_type_emoji):
|
||||
inst = reaction_type_emoji
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {"emoji": self.emoji}
|
||||
reaction_type_emoji = ReactionTypeEmoji.de_json(json_dict, offline_bot)
|
||||
assert reaction_type_emoji.api_kwargs == {}
|
||||
assert reaction_type_emoji.type == self.type
|
||||
assert reaction_type_emoji.emoji == self.emoji
|
||||
|
||||
def test_to_dict(self, reaction_type_emoji):
|
||||
reaction_type_emoji_dict = reaction_type_emoji.to_dict()
|
||||
assert isinstance(reaction_type_emoji_dict, dict)
|
||||
assert reaction_type_emoji_dict["type"] == reaction_type_emoji.type
|
||||
assert reaction_type_emoji_dict["emoji"] == reaction_type_emoji.emoji
|
||||
|
||||
def test_equality(self, reaction_type_emoji):
|
||||
a = reaction_type_emoji
|
||||
b = ReactionTypeEmoji(emoji=self.emoji)
|
||||
c = ReactionTypeEmoji(emoji="other_emoji")
|
||||
d = Dice(5, "test")
|
||||
def test_equality(self, reaction_type):
|
||||
a = ReactionTypeEmoji(emoji=RTDefaults.normal_emoji)
|
||||
b = ReactionTypeEmoji(emoji=RTDefaults.normal_emoji)
|
||||
c = ReactionTypeCustomEmoji(custom_emoji_id=RTDefaults.custom_emoji)
|
||||
d = ReactionTypeCustomEmoji(custom_emoji_id=RTDefaults.custom_emoji)
|
||||
e = ReactionTypeEmoji(emoji=ReactionEmoji.RED_HEART)
|
||||
f = ReactionTypeCustomEmoji(custom_emoji_id="1234custom")
|
||||
g = deepcopy(a)
|
||||
h = deepcopy(c)
|
||||
i = Dice(4, "emoji")
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -128,82 +191,29 @@ class TestReactionTypeEmojiWithoutRequest(ReactionTypeTestBase):
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
assert a == g
|
||||
assert hash(a) == hash(g)
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reaction_type_custom_emoji():
|
||||
return ReactionTypeCustomEmoji(
|
||||
custom_emoji_id=TestReactionTypeCustomEmojiWithoutRequest.custom_emoji_id
|
||||
)
|
||||
assert a != i
|
||||
assert hash(a) != hash(i)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
class TestReactionTypeCustomEmojiWithoutRequest(ReactionTypeTestBase):
|
||||
type = constants.ReactionType.CUSTOM_EMOJI
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
def test_slot_behaviour(self, reaction_type_custom_emoji):
|
||||
inst = reaction_type_custom_emoji
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
assert c != f
|
||||
assert hash(c) != hash(f)
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {"custom_emoji_id": self.custom_emoji_id}
|
||||
reaction_type_custom_emoji = ReactionTypeCustomEmoji.de_json(json_dict, offline_bot)
|
||||
assert reaction_type_custom_emoji.api_kwargs == {}
|
||||
assert reaction_type_custom_emoji.type == self.type
|
||||
assert reaction_type_custom_emoji.custom_emoji_id == self.custom_emoji_id
|
||||
assert c == h
|
||||
assert hash(c) == hash(h)
|
||||
|
||||
def test_to_dict(self, reaction_type_custom_emoji):
|
||||
reaction_type_custom_emoji_dict = reaction_type_custom_emoji.to_dict()
|
||||
assert isinstance(reaction_type_custom_emoji_dict, dict)
|
||||
assert reaction_type_custom_emoji_dict["type"] == reaction_type_custom_emoji.type
|
||||
assert (
|
||||
reaction_type_custom_emoji_dict["custom_emoji_id"]
|
||||
== reaction_type_custom_emoji.custom_emoji_id
|
||||
)
|
||||
|
||||
def test_equality(self, reaction_type_custom_emoji):
|
||||
a = reaction_type_custom_emoji
|
||||
b = ReactionTypeCustomEmoji(custom_emoji_id=self.custom_emoji_id)
|
||||
c = ReactionTypeCustomEmoji(custom_emoji_id="other_custom_emoji_id")
|
||||
d = Dice(5, "test")
|
||||
|
||||
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 reaction_type_paid():
|
||||
return ReactionTypePaid()
|
||||
|
||||
|
||||
class TestReactionTypePaidWithoutRequest(ReactionTypeTestBase):
|
||||
type = constants.ReactionType.PAID
|
||||
|
||||
def test_slot_behaviour(self, reaction_type_paid):
|
||||
inst = reaction_type_paid
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {}
|
||||
reaction_type_paid = ReactionTypePaid.de_json(json_dict, offline_bot)
|
||||
assert reaction_type_paid.api_kwargs == {}
|
||||
assert reaction_type_paid.type == self.type
|
||||
|
||||
def test_to_dict(self, reaction_type_paid):
|
||||
reaction_type_paid_dict = reaction_type_paid.to_dict()
|
||||
assert isinstance(reaction_type_paid_dict, dict)
|
||||
assert reaction_type_paid_dict["type"] == reaction_type_paid.type
|
||||
assert c != i
|
||||
assert hash(c) != hash(i)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
+3
-13
@@ -343,9 +343,9 @@ class TestUserWithoutRequest(UserTestBase):
|
||||
"title",
|
||||
"description",
|
||||
"payload",
|
||||
"provider_token",
|
||||
"currency",
|
||||
"prices",
|
||||
"provider_token",
|
||||
)
|
||||
|
||||
async def test_instance_method_send_location(self, monkeypatch, user):
|
||||
@@ -731,18 +731,8 @@ class TestUserWithoutRequest(UserTestBase):
|
||||
and kwargs["text_entities"] == "text_entities"
|
||||
)
|
||||
|
||||
# TODO discuss if better way exists
|
||||
# tags: deprecated 21.11
|
||||
with pytest.raises(
|
||||
Exception,
|
||||
match="Default for argument gift_id does not match the default of the Bot method.",
|
||||
):
|
||||
assert check_shortcut_signature(
|
||||
user.send_gift, Bot.send_gift, ["user_id", "chat_id"], []
|
||||
)
|
||||
assert await check_shortcut_call(
|
||||
user.send_gift, user.get_bot(), "send_gift", ["chat_id", "user_id"]
|
||||
)
|
||||
assert check_shortcut_signature(user.send_gift, Bot.send_gift, ["user_id"], [])
|
||||
assert await check_shortcut_call(user.send_gift, user.get_bot(), "send_gift")
|
||||
assert await check_defaults_handling(user.send_gift, user.get_bot())
|
||||
|
||||
monkeypatch.setattr(user.get_bot(), "send_gift", make_assertion)
|
||||
|
||||
Reference in New Issue
Block a user