mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-20 08:05:27 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bfbf6d3f94 | |||
| 0c4180c74b | |||
| 1c6ae435bf | |||
| 66b6d3c497 | |||
| 8c252c9822 | |||
| 450dc2115c | |||
| 87a6890900 | |||
| 83ab12c387 | |||
| 3f444dad8d | |||
| f23315d08b | |||
| 7b116be344 | |||
| 934e4c9bd4 | |||
| 1966fb25c5 | |||
| a333d8514a | |||
| b146c7131e | |||
| 53093ebceb | |||
| 401b2decce | |||
| 83a164e5ef | |||
| d91bc45cdc | |||
| 8967912f46 | |||
| 7ab2cafbee | |||
| 7e0ed2235e | |||
| 53abb7b4bd | |||
| 11f86b8813 | |||
| 9997a9f47e |
@@ -67,6 +67,7 @@ Here's how to make a one-off code change.
|
||||
$ git checkout -b your-branch-name
|
||||
|
||||
3. **Make a commit to your feature branch**. Each commit should be self-contained and have a descriptive commit message that helps other developers understand why the changes were made.
|
||||
We also have a check-list for PRs `below`_.
|
||||
|
||||
- You can refer to relevant issues in the commit message by writing, e.g., "#105".
|
||||
|
||||
@@ -80,7 +81,7 @@ Here's how to make a one-off code change.
|
||||
|
||||
- The following exceptions to the above (Google's) style guides applies:
|
||||
|
||||
- Documenting types of global variables and complex types of class members can be done using the Sphinx docstring convention.
|
||||
- Documenting types of global variables and complex types of class members can be done using the Sphinx docstring convention.
|
||||
|
||||
- In addition, PTB uses some formatting/styling and linting tools in the pre-commit setup. Some of those tools also have command line tools that can help to run these tools outside of the pre-commit step. If you'd like to leverage that, please have a look at the `pre-commit config file`_ for an overview of which tools (and which versions of them) are used. For example, we use `Black`_ for code formatting. Plugins for Black exist for some `popular editors`_. You can use those instead of manually formatting everything.
|
||||
|
||||
@@ -121,11 +122,11 @@ Here's how to make a one-off code change.
|
||||
|
||||
- When your reviewer has reviewed the code, you'll get a notification. You'll need to respond in two ways:
|
||||
|
||||
- Make a new commit addressing the comments you agree with, and push it to the same branch. Ideally, the commit message would explain what the commit does (e.g. "Fix lint error"), but if there are lots of disparate review comments, it's fine to refer to the original commit message and add something like "(address review comments)".
|
||||
- Make a new commit addressing the comments you agree with, and push it to the same branch. Ideally, the commit message would explain what the commit does (e.g. "Fix lint error"), but if there are lots of disparate review comments, it's fine to refer to the original commit message and add something like "(address review comments)".
|
||||
|
||||
- In order to keep the commit history intact, please avoid squashing or amending history and then force-pushing to the PR. Reviewers often want to look at individual commits.
|
||||
- In order to keep the commit history intact, please avoid squashing or amending history and then force-pushing to the PR. Reviewers often want to look at individual commits.
|
||||
|
||||
- In addition, please reply to each comment. Each reply should be either "Done" or a response explaining why the corresponding suggestion wasn't implemented. All comments must be resolved before LGTM can be given.
|
||||
- In addition, please reply to each comment. Each reply should be either "Done" or a response explaining why the corresponding suggestion wasn't implemented. All comments must be resolved before LGTM can be given.
|
||||
|
||||
- Resolve any merge conflicts that arise. To resolve conflicts between 'your-branch-name' (in your fork) and 'master' (in the ``python-telegram-bot`` repository), run:
|
||||
|
||||
@@ -150,6 +151,51 @@ Here's how to make a one-off code change.
|
||||
|
||||
7. **Celebrate.** Congratulations, you have contributed to ``python-telegram-bot``!
|
||||
|
||||
Check-list for PRs
|
||||
------------------
|
||||
|
||||
This checklist is a non-exhaustive reminder of things that should be done before a PR is merged, both for you as contributor and for the maintainers.
|
||||
Feel free to copy (parts of) the checklist to the PR description to remind you or the maintainers of open points or if you have questions on anything.
|
||||
|
||||
- Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION`` or ``.. deprecated:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
|
||||
- Created new or adapted existing unit tests
|
||||
- Documented code changes according to the `CSI standard <https://standards.mousepawmedia.com/en/stable/csi.html>`__
|
||||
- Added myself alphabetically to ``AUTHORS.rst`` (optional)
|
||||
- Added new classes & modules to the docs and all suitable ``__all__`` s
|
||||
- Checked the `Stability Policy <https://docs.python-telegram-bot.org/stability_policy.html>`_ in case of deprecations or changes to documented behavior
|
||||
|
||||
**If the PR contains API changes (otherwise, you can ignore this passage)**
|
||||
|
||||
- Checked the Bot API specific sections of the `Stability Policy <https://docs.python-telegram-bot.org/stability_policy.html>`_
|
||||
|
||||
- New classes:
|
||||
|
||||
- Added ``self._id_attrs`` and corresponding documentation
|
||||
- ``__init__`` accepts ``api_kwargs`` as kw-only
|
||||
|
||||
- Added new shortcuts:
|
||||
|
||||
- In :class:`~telegram.Chat` & :class:`~telegram.User` for all methods that accept ``chat/user_id``
|
||||
- In :class:`~telegram.Message` for all methods that accept ``chat_id`` and ``message_id``
|
||||
- For new :class:`~telegram.Message` shortcuts: Added ``quote`` argument if methods accepts ``reply_to_message_id``
|
||||
- In :class:`~telegram.CallbackQuery` for all methods that accept either ``chat_id`` and ``message_id`` or ``inline_message_id``
|
||||
|
||||
- If relevant:
|
||||
|
||||
- Added new constants at :mod:`telegram.constants` and shortcuts to them as class variables
|
||||
- Link new and existing constants in docstrings instead of hard-coded numbers and strings
|
||||
- Add new message types to :attr:`telegram.Message.effective_attachment`
|
||||
- Added new handlers for new update types
|
||||
|
||||
- Add the handlers to the warning loop in the :class:`~telegram.ext.ConversationHandler`
|
||||
|
||||
- Added new filters for new message (sub)types
|
||||
- Added or updated documentation for the changed class(es) and/or method(s)
|
||||
- Added the new method(s) to ``_extbot.py``
|
||||
- Added or updated ``bot_methods.rst``
|
||||
- Updated the Bot API version number in all places: ``README.rst`` and ``README_RAW.rst`` (including the badge), as well as ``telegram.constants.BOT_API_VERSION_INFO``
|
||||
- Added logic for arbitrary callback data in :class:`telegram.ext.ExtBot` for new methods that either accept a ``reply_markup`` in some form or have a return type that is/contains :class:`~telegram.Message`
|
||||
|
||||
Documenting
|
||||
===========
|
||||
|
||||
@@ -266,3 +312,4 @@ break the API classes. For example:
|
||||
.. _`CSI`: https://standards.mousepawmedia.com/en/stable/csi.html
|
||||
.. _`section`: #documenting
|
||||
.. _`testing page`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/tests/README.rst
|
||||
.. _`below`: #check-list-for-prs
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
<!--
|
||||
Hey! You're PRing? Cool! Please have a look at the below checklist. It's here to help both you and the maintainers to remember some aspects. Make sure to check out our contribution guide (https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.github/CONTRIBUTING.rst).
|
||||
Hey! You're PRing? Cool!
|
||||
Please be sure to check out our contribution guide (https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.github/CONTRIBUTING.rst).
|
||||
Especially, please have a look at the check list for PRs (https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.github/CONTRIBUTING.rst#checklist-for-prs). Feel free to copy (parts of) the checklist to the PR description to remind you or the maintainers of open points or if you have questions on anything.
|
||||
-->
|
||||
|
||||
### Checklist for PRs
|
||||
|
||||
- [ ] Added `.. versionadded:: version`, `.. versionchanged:: version` or `.. deprecated:: version` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
|
||||
- [ ] Created new or adapted existing unit tests
|
||||
- [ ] Documented code changes according to the [CSI standard](https://standards.mousepawmedia.com/en/stable/csi.html)
|
||||
- [ ] Added myself alphabetically to `AUTHORS.rst` (optional)
|
||||
- [ ] Added new classes & modules to the docs and all suitable `__all__` s
|
||||
|
||||
|
||||
### If the PR contains API changes (otherwise, you can delete this passage)
|
||||
|
||||
* New classes:
|
||||
- [ ] Added `self._id_attrs` and corresponding documentation
|
||||
- [ ] `__init__` accepts `api_kwargs` as kw-only
|
||||
|
||||
* Added new shortcuts:
|
||||
- [ ] In `Chat` & `User` for all methods that accept `chat/user_id`
|
||||
- [ ] In `Message` for all methods that accept `chat_id` and `message_id`
|
||||
- [ ] For new `Message` shortcuts: Added `quote` argument if methods accepts `reply_to_message_id`
|
||||
- [ ] In `CallbackQuery` for all methods that accept either `chat_id` and `message_id` or `inline_message_id`
|
||||
|
||||
* If relevant:
|
||||
|
||||
- [ ] Added new constants at `telegram.constants` and shortcuts to them as class variables
|
||||
- [ ] Link new and existing constants in docstrings instead of hard coded number and strings
|
||||
- [ ] Add new message types to `Message.effective_attachment`
|
||||
- [ ] Added new handlers for new update types
|
||||
- [ ] Add the handlers to the warning loop in the `ConversationHandler`
|
||||
- [ ] Added new filters for new message (sub)types
|
||||
- [ ] Added or updated documentation for the changed class(es) and/or method(s)
|
||||
- [ ] Added the new method(s) to `_extbot.py`
|
||||
- [ ] Added or updated `bot_methods.rst`
|
||||
- [ ] Updated the Bot API version number in all places: `README.rst` and `README_RAW.rst` (including the badge), as well as `telegram.constants.BOT_API_VERSION_INFO`
|
||||
- [ ] Added logic for arbitrary callback data in `tg.ext.Bot` for new methods that either accept a `reply_markup` in some form or have a return type that is/contains `telegram.Message`
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v7
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
# PRs never get stale
|
||||
days-before-stale: 3
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -49,21 +49,21 @@ jobs:
|
||||
pr = float(
|
||||
json.load(open("pr.json", "rb"))["typeCompleteness"]["completenessScore"]
|
||||
)
|
||||
base_text = f"After this PR, type completeness will be {round(pr, 3)}."
|
||||
if pr < (base - 0.1):
|
||||
text = f"This PR decreases type completeness by {round(base - pr, 3)}. ❌"
|
||||
base_text = f"This PR changes type completeness from {round(base, 3)} to {round(pr, 3)}."
|
||||
if pr < (base - 0.001):
|
||||
text = f"{base_text} ❌"
|
||||
set_summary(text)
|
||||
print(Path("pr.readable").read_text(encoding="utf-8"))
|
||||
error(f"{text}\n{base_text}")
|
||||
error(text)
|
||||
exit(1)
|
||||
elif pr > (base + 0.1):
|
||||
text = f"This PR increases type completeness by {round(pr - base, 3)}. ✨"
|
||||
elif pr > (base + 0.001):
|
||||
text = f"{base_text} ✨"
|
||||
set_summary(text)
|
||||
if pr < 1:
|
||||
print(Path("pr.readable").read_text(encoding="utf-8"))
|
||||
print(f"{text}\n{base_text}")
|
||||
print(text)
|
||||
else:
|
||||
text = f"This PR does not change type completeness by more than 0.1. ✅"
|
||||
text = f"{base_text} This is less than 0.1 percentage points. ✅"
|
||||
set_summary(text)
|
||||
print(Path("pr.readable").read_text(encoding="utf-8"))
|
||||
print(f"{text}\n{base_text}")
|
||||
print(text)
|
||||
|
||||
@@ -9,7 +9,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
@@ -20,7 +20,7 @@ repos:
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/PyCQA/pylint
|
||||
rev: v2.16.4
|
||||
rev: v3.0.0a6
|
||||
hooks:
|
||||
- id: pylint
|
||||
files: ^(telegram|examples)/.*\.py$
|
||||
@@ -38,7 +38,7 @@ repos:
|
||||
- aiolimiter~=1.0.0
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.0.1
|
||||
rev: v1.2.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy-ptb
|
||||
@@ -65,7 +65,7 @@ repos:
|
||||
- cachetools~=5.3.0
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v3.3.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
files: ^(telegram|examples|tests|docs)/.*\.py$
|
||||
@@ -80,11 +80,11 @@ repos:
|
||||
- --diff
|
||||
- --check
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.254'
|
||||
rev: 'v0.0.263'
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff
|
||||
files: ^(telegram|examples)/.*\.py$
|
||||
files: ^(telegram|examples|tests)/.*\.py$
|
||||
additional_dependencies:
|
||||
- httpx~=0.23.3
|
||||
- tornado~=6.2
|
||||
|
||||
@@ -72,6 +72,7 @@ The following wonderful people contributed directly or indirectly to this projec
|
||||
- `Li-aung Yip <https://github.com/LiaungYip>`_
|
||||
- `Loo Zheng Yuan <https://github.com/loozhengyuan>`_
|
||||
- `LRezende <https://github.com/lrezende>`_
|
||||
- `Luca Bellanti <https://github.com/Trifase>`_
|
||||
- `macrojames <https://github.com/macrojames>`_
|
||||
- `Matheus Lemos <https://github.com/mlemosf>`_
|
||||
- `Michael Dix <https://github.com/Eisberge>`_
|
||||
|
||||
+74
@@ -1,7 +1,81 @@
|
||||
.. _ptb-changelog:
|
||||
|
||||
=========
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 20.3
|
||||
============
|
||||
*Released 2023-05-07*
|
||||
|
||||
This is the technical changelog for version 20.3. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Full support for API 6.7 (`#3673`_)
|
||||
- Add a Stability Policy (`#3622`_)
|
||||
|
||||
New Features
|
||||
------------
|
||||
|
||||
- Add ``Application.mark_data_for_update_persistence`` (`#3607`_)
|
||||
- Make ``Message.link`` Point to Thread View Where Possible (`#3640`_)
|
||||
- Localize Received ``datetime`` Objects According to ``Defaults.tzinfo`` (`#3632`_)
|
||||
|
||||
Minor Changes, Documentation Improvements and CI
|
||||
------------------------------------------------
|
||||
|
||||
- Empower ``ruff`` (`#3594`_)
|
||||
- Drop Usage of ``sys.maxunicode`` (`#3630`_)
|
||||
- Add String Representation for ``RequestParameter`` (`#3634`_)
|
||||
- Stabilize CI by Rerunning Failed Tests (`#3631`_)
|
||||
- Give Loggers Better Names (`#3623`_)
|
||||
- Add Logging for Invalid JSON Data in ``BasePersistence.parse_json_payload`` (`#3668`_)
|
||||
- Improve Warning Categories & Stacklevels (`#3674`_)
|
||||
- Stabilize ``test_delete_sticker_set`` (`#3685`_)
|
||||
- Shield Update Fetcher Task in ``Application.start`` (`#3657`_)
|
||||
- Recover 100% Type Completeness (`#3676`_)
|
||||
- Documentation Improvements (`#3628`_, `#3636`_, `#3694`_)
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
- Bump ``actions/stale`` from 7 to 8 (`#3644`_)
|
||||
- Bump ``furo`` from 2023.3.23 to 2023.3.27 (`#3643`_)
|
||||
- ``pre-commit`` autoupdate (`#3646`_, `#3688`_)
|
||||
- Remove Deprecated ``codecov`` Package from CI (`#3664`_)
|
||||
- Bump ``sphinx-copybutton`` from 0.5.1 to 0.5.2 (`#3662`_)
|
||||
- Update ``httpx`` requirement from ~=0.23.3 to ~=0.24.0 (`#3660`_)
|
||||
- Bump ``pytest`` from 7.2.2 to 7.3.1 (`#3661`_)
|
||||
|
||||
.. _`#3673`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3673
|
||||
.. _`#3622`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3622
|
||||
.. _`#3607`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3607
|
||||
.. _`#3640`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3640
|
||||
.. _`#3632`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3632
|
||||
.. _`#3594`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3594
|
||||
.. _`#3630`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3630
|
||||
.. _`#3634`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3634
|
||||
.. _`#3631`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3631
|
||||
.. _`#3623`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3623
|
||||
.. _`#3668`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3668
|
||||
.. _`#3674`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3674
|
||||
.. _`#3685`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3685
|
||||
.. _`#3657`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3657
|
||||
.. _`#3676`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3676
|
||||
.. _`#3628`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3628
|
||||
.. _`#3636`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3636
|
||||
.. _`#3694`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3694
|
||||
.. _`#3644`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3644
|
||||
.. _`#3643`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3643
|
||||
.. _`#3646`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3646
|
||||
.. _`#3688`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3688
|
||||
.. _`#3664`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3664
|
||||
.. _`#3662`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3662
|
||||
.. _`#3660`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3660
|
||||
.. _`#3661`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3661
|
||||
|
||||
Version 20.2
|
||||
============
|
||||
*Released 2023-03-25*
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@
|
||||
:target: https://pypi.org/project/python-telegram-bot/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.6-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.7-blue?logo=telegram
|
||||
:target: https://core.telegram.org/bots/api-changelog
|
||||
:alt: Supported Bot API versions
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
:target: https://app.codacy.com/gh/python-telegram-bot/python-telegram-bot/dashboard
|
||||
:alt: Code quality: Codacy
|
||||
|
||||
.. image:: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues
|
||||
:target: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge
|
||||
.. image:: https://app.deepsource.com/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues
|
||||
:target: https://app.deepsource.com/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge
|
||||
:alt: Code quality: DeepSource
|
||||
|
||||
.. image:: https://results.pre-commit.ci/badge/github/python-telegram-bot/python-telegram-bot/master.svg
|
||||
@@ -93,7 +93,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
|
||||
Telegram API support
|
||||
====================
|
||||
|
||||
All types and methods of the Telegram Bot API **6.6** are supported.
|
||||
All types and methods of the Telegram Bot API **6.7** are supported.
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@
|
||||
:target: https://pypi.org/project/python-telegram-bot-raw/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.6-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.7-blue?logo=telegram
|
||||
:target: https://core.telegram.org/bots/api-changelog
|
||||
:alt: Supported Bot API versions
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
:target: https://app.codacy.com/gh/python-telegram-bot/python-telegram-bot/dashboard
|
||||
:alt: Code quality: Codacy
|
||||
|
||||
.. image:: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues
|
||||
:target: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge
|
||||
.. image:: https://app.deepsource.com/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues
|
||||
:target: https://app.deepsource.com/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge
|
||||
:alt: Code quality: DeepSource
|
||||
|
||||
.. image:: https://results.pre-commit.ci/badge/github/python-telegram-bot/python-telegram-bot/master.svg
|
||||
@@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
|
||||
Telegram API support
|
||||
====================
|
||||
|
||||
All types and methods of the Telegram Bot API **6.6** are supported.
|
||||
All types and methods of the Telegram Bot API **6.7** are supported.
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
sphinx==6.1.3
|
||||
sphinx-pypi-upload
|
||||
furo==2023.3.23
|
||||
furo==2023.3.27
|
||||
git+https://github.com/harshil21/furo-sphinx-search@01efc7be422d7dc02390aab9be68d6f5ce1a5618#egg=furo-sphinx-search
|
||||
sphinx-paramlinks==0.5.4
|
||||
sphinxcontrib-mermaid==0.8.1
|
||||
sphinx-copybutton==0.5.1
|
||||
sphinx-copybutton==0.5.2
|
||||
|
||||
+2
-2
@@ -21,9 +21,9 @@ author = "Leandro Toledo"
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "20.2" # telegram.__version__[:3]
|
||||
version = "20.3" # telegram.__version__[:3]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "20.2" # telegram.__version__
|
||||
release = "20.3" # telegram.__version__
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
needs_sphinx = "6.1.3"
|
||||
|
||||
@@ -8,7 +8,7 @@ aspect of the Telegram Bot API while others focus on one of the
|
||||
mechanics of this library. Except for the
|
||||
:any:`examples.rawapibot` example, they all use the high-level
|
||||
framework this library provides with the
|
||||
:any:`telegram.ext <telegram.ext>` submodule.
|
||||
:mod:`telegram.ext` submodule.
|
||||
|
||||
All examples are licensed under the `CC0
|
||||
License <https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/LICENSE.txt>`__
|
||||
|
||||
@@ -196,6 +196,10 @@
|
||||
- Used for setting the short description of the bot
|
||||
* - :meth:`~telegram.Bot.get_my_short_description`
|
||||
- Used for obtaining the short description of the bot
|
||||
* - :meth:`~telegram.Bot.set_my_name`
|
||||
- Used for setting the name of the bot
|
||||
* - :meth:`~telegram.Bot.get_my_name`
|
||||
- Used for obtaining the name of the bot
|
||||
|
||||
.. raw:: html
|
||||
|
||||
|
||||
@@ -27,11 +27,12 @@
|
||||
:hidden:
|
||||
:caption: Project
|
||||
|
||||
stability_policy
|
||||
changelog
|
||||
coc
|
||||
contributing
|
||||
testing
|
||||
Website <https://python-telegram-bot.org>
|
||||
GitHub Repository <https://github.com/python-telegram-bot/python-telegram-bot/>
|
||||
Telegram Channel <https://t.me/pythontelegrambotchannel/>
|
||||
Telegram User Group <https://t.me/pythontelegrambotgroup/>
|
||||
coc
|
||||
contributing
|
||||
testing
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
Stability Policy
|
||||
================
|
||||
|
||||
.. important::
|
||||
|
||||
This stability policy is in place since version 20.3.
|
||||
While earlier versions of ``python-telegram-bot`` also had stable interfaces, they had no explicit stability policy and hence did not follow the rules outlined below in all detail.
|
||||
Please also refer to the :ref:`changelog <ptb-changelog>`.
|
||||
|
||||
.. caution::
|
||||
|
||||
Large parts of the :mod:`telegram` package are the Python representations of the Telegram Bot API, whose stability policy PTB can not influence.
|
||||
This policy hence includes some special cases for those parts.
|
||||
|
||||
What does this policy cover?
|
||||
----------------------------
|
||||
|
||||
This policy includes any API or behavior that is covered in this documentation.
|
||||
This covers both the :mod:`telegram` package and the :mod:`telegram.ext` package.
|
||||
|
||||
What doesn't this policy cover?
|
||||
-------------------------------
|
||||
|
||||
Introduction of new features or changes of flavors of comparable behavior (e.g. the default for the HTTP protocol version being used) are not covered by this policy.
|
||||
|
||||
The internal structure of classes in PTB, i.e. things like the result of ``dir(obj))`` or the contents of ``obj.__dict__``, is not covered by this policy.
|
||||
|
||||
Objects are in general not guaranteed to be pickleable (unless stated otherwise) and pickled objects from one version of PTB may not be loadable in future versions.
|
||||
We may provide a way to convert pickled objects from one version to another, but this is not guaranteed.
|
||||
|
||||
Functionality that is part of PTBs API but is explicitly documented as not being intended to be used directly by users (e.g. :meth:`telegram.request.BaseRequest.do_request`) may change.
|
||||
This also applies to functions or attributes marked as final in the sense of `PEP 591 <https://www.python.org/dev/peps/pep-0591/>`__.
|
||||
|
||||
PTB has dependencies to third-party packages.
|
||||
The versions that PTB uses of these third-party packages may change if that does not affect PTBs public API.
|
||||
|
||||
PTB does not give guarantees about which Python versions are supported.
|
||||
In general, we will try to support all Python versions that have not yet reached their end of life, but we reserve ourselves the option to drop support for Python versions earlier if that benefits the advancement of the library.
|
||||
|
||||
.. _bot-api-functionality-1:
|
||||
|
||||
Bot API Functionality
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Comparison of equality of instances of the classes in the :mod:`telegram` package is subject to change and the PTB team will update the behavior to best reflect updates in the Bot API.
|
||||
Changes in this regard will be documented in the affected classes.
|
||||
Note that equality comparison with objects that where serialized by an older version of PTB may hence give unexpected results.
|
||||
|
||||
When the order of arguments of the Bot API methods changes or they become optional/mandatory due to changes in the Bot API, PTB will always try to reflect these changes.
|
||||
While we try to make such changes backward compatible, this is not always possible or only with significant effort.
|
||||
In such cases we will find a trade-off between backward compatibility and fully complying with the Bot API, which may result in breaking changes.
|
||||
We highly recommend using keyword arguments, which can help make such changes non-breaking on your end.
|
||||
|
||||
..
|
||||
We have documented a few common cases and possible backwards compatible solutions in the wiki as a reference for the dev team: https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Backward-Compatibility
|
||||
|
||||
When the Bot API changes attributes of classes, the method :meth:`telegram.TelegramObject.to_dict` will change as necessary to reflect these changes.
|
||||
In particular, attributes deprecated by Telegram will be removed from the returned dictionary.
|
||||
Deprecated attributes that are still passed by Telegram will be available in the :attr:`~telegram.TelegramObject.api_kwargs` dictionary as long as PTB can support that with feasible effort.
|
||||
Since attributes of the classes in the :mod:`telegram` package are not writable, we may change them to properties where appropriate.
|
||||
|
||||
Development Versions
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Pre-releases marked as alpha, beta or release candidate are not covered by this policy.
|
||||
Before a feature is in a stable release, i.e. the feature was merged into the ``master`` branch but not released yet (or only in a pre-release), it is not covered by this policy either and may change.
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
We make exceptions from our stability policy for security.
|
||||
We will violate this policy as necessary in order to resolve a security issue or harden PTB against a possible attack.
|
||||
|
||||
Versioning
|
||||
----------
|
||||
|
||||
PTB uses a versioning scheme that roughly follows `https://semver.org/ <https://semver.org/>`_, although it may not be quite as strict.
|
||||
|
||||
Given a version of PTB X.Y.Z,
|
||||
|
||||
- X indicates the major version number.
|
||||
This is incremented when backward incompatible changes are introduced.
|
||||
- Y indicates the minor version number.
|
||||
This is incremented when new functionality or backward compatible changes are introduced by PTB.
|
||||
*This is also incremented when PTB adds support for a new Bot API version, which may include backward incompatible changes in some cases as outlined* :ref:`below <bot-api-versioning>`.
|
||||
- Z is the patch version.
|
||||
This is incremented if backward compatible bug fixes or smaller changes are introduced.
|
||||
If this number is 0, it can be omitted, i.e. we just write X.Y instead of X.Y.0.
|
||||
|
||||
Deprecation
|
||||
~~~~~~~~~~~
|
||||
|
||||
From time to time we will want to change the behavior of an API or remove it entirely, or we do so to comply with changes in the Telegram Bot API.
|
||||
In those cases, we follow a deprecation schedule as detailed below.
|
||||
|
||||
Functionality is marked as deprecated by a corresponding note in the release notes and the documentation.
|
||||
Where possible, a :class:`~telegram.warnings.PTBDeprecationWarning` is issued when deprecated functionality is used, but this is not mandatory.
|
||||
|
||||
From time to time, we may decide to deprecate an API that is particularly widely used.
|
||||
In these cases, we may decide to provide an extended deprecation period, at our discretion.
|
||||
|
||||
With version 20.0.0, PTB introduced major structural breaking changes without the above deprecation period.
|
||||
Should a similarly big change ever be deemed necessary again by the development team and should a deprecation period prove too much additional effort, this violation of the stability policy will be announced well ahead of the release in our channel, `as was done for v20 <https://t.me/pythontelegrambotchannel/94>`_.
|
||||
|
||||
Non-Bot API Functionality
|
||||
#########################
|
||||
|
||||
Starting with version 20.3, deprecated functionality will stay available for the current and the next major version.
|
||||
For example:
|
||||
|
||||
- In PTB v20.1.1 the feature exists
|
||||
- In PTB v20.1.2 or v20.2.0 the feature is marked as deprecated
|
||||
- In PTB v21.*.* the feature is marked as deprecated
|
||||
- In PTB v22.0 the feature is removed or changed
|
||||
|
||||
.. _bot-api-versioning:
|
||||
|
||||
Bot API Functionality
|
||||
#####################
|
||||
|
||||
As PTB has no control over deprecations introduced by Telegram and the schedule of these deprecations rarely coincides with PTBs deprecation schedule, we have a special policy for Bot API functionality.
|
||||
|
||||
Starting with 20.3, deprecated Bot API functionality will stay available for the current and the next major version of PTB *or* until the next version of the Bot API.
|
||||
More precisely, two cases are possible, for which we show examples below.
|
||||
|
||||
Case 1
|
||||
^^^^^^
|
||||
|
||||
- In PTB v20.1 the feature exists
|
||||
- Bot API version 6.6 is released and deprecates the feature
|
||||
- PTB v20.2 adds support for Bot API 6.6 and the feature is
|
||||
marked as deprecated
|
||||
- In PTB v21.0 the feature is removed or changed
|
||||
|
||||
Case 2
|
||||
^^^^^^
|
||||
|
||||
- In PTB v20.1 the feature exists
|
||||
- Bot API version 6.6 is released and deprecates the feature
|
||||
- PTB v20.2 adds support for Bot API version 6.6 and the feature is marked as deprecated
|
||||
- In PTB v20.2.* and v20.3.* the feature is marked as deprecated
|
||||
- Bot API version 6.7 is released
|
||||
- PTB v20.4 adds support for Bot API version 6.7 and the feature is removed or changed
|
||||
@@ -16,6 +16,7 @@ Available Types
|
||||
telegram.botcommandscopechatmember
|
||||
telegram.botcommandscopedefault
|
||||
telegram.botdescription
|
||||
telegram.botname
|
||||
telegram.botshortdescription
|
||||
telegram.callbackquery
|
||||
telegram.chat
|
||||
@@ -78,6 +79,7 @@ Available Types
|
||||
telegram.replykeyboardmarkup
|
||||
telegram.replykeyboardremove
|
||||
telegram.sentwebappmessage
|
||||
telegram.switchinlinequerychosenchat
|
||||
telegram.telegramobject
|
||||
telegram.update
|
||||
telegram.user
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
BotName
|
||||
=======
|
||||
|
||||
.. autoclass:: telegram.BotName
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -1,6 +1,8 @@
|
||||
telegram.ext package
|
||||
====================
|
||||
|
||||
.. automodule:: telegram.ext
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ Inline Mode
|
||||
telegram.inlinequeryresultlocation
|
||||
telegram.inlinequeryresultmpeg4gif
|
||||
telegram.inlinequeryresultphoto
|
||||
telegram.inlinequeryresultsbutton
|
||||
telegram.inlinequeryresultvenue
|
||||
telegram.inlinequeryresultvideo
|
||||
telegram.inlinequeryresultvoice
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
InlineQueryResultsButton
|
||||
========================
|
||||
|
||||
.. autoclass:: telegram.InlineQueryResultsButton
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
SwitchInlineQueryChosenChat
|
||||
===========================
|
||||
|
||||
.. autoclass:: telegram.SwitchInlineQueryChosenChat
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -55,3 +55,5 @@
|
||||
.. |sequenceargs| replace:: Accepts any :class:`collections.abc.Sequence` as input instead of just a list.
|
||||
|
||||
.. |captionentitiesattr| replace:: Tuple of special entities that appear in the caption, which can be specified instead of ``parse_mode``.
|
||||
|
||||
.. |datetime_localization| replace:: The default timezone of the bot is used for localization, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
@@ -103,13 +103,12 @@ async def track_chats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
|
||||
elif was_member and not is_member:
|
||||
logger.info("%s removed the bot from the group %s", cause_name, chat.title)
|
||||
context.bot_data.setdefault("group_ids", set()).discard(chat.id)
|
||||
else:
|
||||
if not was_member and is_member:
|
||||
logger.info("%s added the bot to the channel %s", cause_name, chat.title)
|
||||
context.bot_data.setdefault("channel_ids", set()).add(chat.id)
|
||||
elif was_member and not is_member:
|
||||
logger.info("%s removed the bot from the channel %s", cause_name, chat.title)
|
||||
context.bot_data.setdefault("channel_ids", set()).discard(chat.id)
|
||||
elif not was_member and is_member:
|
||||
logger.info("%s added the bot to the channel %s", cause_name, chat.title)
|
||||
context.bot_data.setdefault("channel_ids", set()).add(chat.id)
|
||||
elif was_member and not is_member:
|
||||
logger.info("%s removed the bot from the channel %s", cause_name, chat.title)
|
||||
context.bot_data.setdefault("channel_ids", set()).discard(chat.id)
|
||||
|
||||
|
||||
async def show_chats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
@@ -39,7 +39,7 @@ DEVELOPER_CHAT_ID = 123456789
|
||||
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Log the error and send a telegram message to notify the developer."""
|
||||
# Log the error before we do anything else, so we can see it even if something breaks.
|
||||
logger.error(msg="Exception while handling an update:", exc_info=context.error)
|
||||
logger.error("Exception while handling an update:", exc_info=context.error)
|
||||
|
||||
# traceback.format_exception returns the usual python message about an exception, but as a
|
||||
# list of strings rather than a single string, so we have to join them together.
|
||||
|
||||
@@ -58,7 +58,7 @@ async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
"""Handle the inline query. This is run when you type: @botusername <query>"""
|
||||
query = update.inline_query.query
|
||||
|
||||
if query == "":
|
||||
if not query: # empty query should not be handled
|
||||
return
|
||||
|
||||
results = [
|
||||
|
||||
+5
-2
@@ -48,6 +48,9 @@ logging.basicConfig(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TOTAL_VOTER_COUNT = 3
|
||||
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Inform user about what this bot can do"""
|
||||
await update.message.reply_text(
|
||||
@@ -101,7 +104,7 @@ async def receive_poll_answer(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
)
|
||||
answered_poll["answers"] += 1
|
||||
# Close poll after three participants voted
|
||||
if answered_poll["answers"] == 3:
|
||||
if answered_poll["answers"] == TOTAL_VOTER_COUNT:
|
||||
await context.bot.stop_poll(answered_poll["chat_id"], answered_poll["message_id"])
|
||||
|
||||
|
||||
@@ -123,7 +126,7 @@ async def receive_quiz_answer(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
# the bot can receive closed poll updates we don't care about
|
||||
if update.poll.is_closed:
|
||||
return
|
||||
if update.poll.total_voter_count == 3:
|
||||
if update.poll.total_voter_count == TOTAL_VOTER_COUNT:
|
||||
try:
|
||||
quiz_data = context.bot_data[update.poll.id]
|
||||
# this means this poll answer update is from an old poll, we can't stop it then
|
||||
|
||||
@@ -7,6 +7,7 @@ on the telegram.ext bot framework.
|
||||
This program is dedicated to the public domain under the CC0 license.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import NoReturn
|
||||
|
||||
@@ -72,7 +73,5 @@ async def echo(bot: Bot, update_id: int) -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
with contextlib.suppress(KeyboardInterrupt): # Ignore exception when Ctrl-C is pressed
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt: # Ignore exception when Ctrl-C is pressed
|
||||
pass
|
||||
|
||||
@@ -9,3 +9,10 @@ line_length = 99
|
||||
[tool.ruff]
|
||||
line-length = 99
|
||||
target-version = "py37"
|
||||
show-fixes = true
|
||||
ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915"]
|
||||
select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE",
|
||||
"G", "ISC", "PT"]
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"tests/*.py" = ["B018"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pre-commit # needed for pre-commit hooks in the git commit command
|
||||
|
||||
# For the test suite
|
||||
pytest==7.2.2
|
||||
pytest==7.3.1
|
||||
pytest-asyncio==0.21.0 # needed because pytest doesn't come with native support for coroutines as tests
|
||||
pytest-xdist==3.2.1 # xdist runs tests in parallel
|
||||
flaky # Used for flaky tests (flaky decorator)
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
# versions and only increase the lower bound if necessary
|
||||
|
||||
# httpx has no stable release yet, so let's be cautious for now
|
||||
httpx ~= 0.23.3
|
||||
httpx ~= 0.24.0
|
||||
|
||||
@@ -38,6 +38,7 @@ __all__ = ( # Keep this alphabetically ordered
|
||||
"BotCommandScopeChatMember",
|
||||
"BotCommandScopeDefault",
|
||||
"BotDescription",
|
||||
"BotName",
|
||||
"BotShortDescription",
|
||||
"CallbackGame",
|
||||
"CallbackQuery",
|
||||
@@ -102,6 +103,7 @@ __all__ = ( # Keep this alphabetically ordered
|
||||
"InlineQueryResultLocation",
|
||||
"InlineQueryResultMpeg4Gif",
|
||||
"InlineQueryResultPhoto",
|
||||
"InlineQueryResultsButton",
|
||||
"InlineQueryResultVenue",
|
||||
"InlineQueryResultVideo",
|
||||
"InlineQueryResultVoice",
|
||||
@@ -169,6 +171,7 @@ __all__ = ( # Keep this alphabetically ordered
|
||||
"Sticker",
|
||||
"StickerSet",
|
||||
"SuccessfulPayment",
|
||||
"SwitchInlineQueryChosenChat",
|
||||
"TelegramObject",
|
||||
"Update",
|
||||
"User",
|
||||
@@ -204,6 +207,7 @@ from ._botcommandscope import (
|
||||
BotCommandScopeDefault,
|
||||
)
|
||||
from ._botdescription import BotDescription, BotShortDescription
|
||||
from ._botname import BotName
|
||||
from ._callbackquery import CallbackQuery
|
||||
from ._chat import Chat
|
||||
from ._chatadministratorrights import ChatAdministratorRights
|
||||
@@ -280,6 +284,7 @@ from ._inline.inlinequeryresultgif import InlineQueryResultGif
|
||||
from ._inline.inlinequeryresultlocation import InlineQueryResultLocation
|
||||
from ._inline.inlinequeryresultmpeg4gif import InlineQueryResultMpeg4Gif
|
||||
from ._inline.inlinequeryresultphoto import InlineQueryResultPhoto
|
||||
from ._inline.inlinequeryresultsbutton import InlineQueryResultsButton
|
||||
from ._inline.inlinequeryresultvenue import InlineQueryResultVenue
|
||||
from ._inline.inlinequeryresultvideo import InlineQueryResultVideo
|
||||
from ._inline.inlinequeryresultvoice import InlineQueryResultVoice
|
||||
@@ -336,6 +341,7 @@ from ._replykeyboardmarkup import ReplyKeyboardMarkup
|
||||
from ._replykeyboardremove import ReplyKeyboardRemove
|
||||
from ._sentwebappmessage import SentWebAppMessage
|
||||
from ._shared import ChatShared, UserShared
|
||||
from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat
|
||||
from ._telegramobject import TelegramObject
|
||||
from ._update import Update
|
||||
from ._user import User
|
||||
|
||||
+253
-171
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,7 @@ class BotDescription(TelegramObject):
|
||||
|
||||
def __init__(self, description: str, *, api_kwargs: JSONDict = None):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
self.description = description
|
||||
self.description: str = description
|
||||
|
||||
self._id_attrs = (self.description,)
|
||||
|
||||
@@ -68,7 +68,7 @@ class BotShortDescription(TelegramObject):
|
||||
|
||||
def __init__(self, short_description: str, *, api_kwargs: JSONDict = None):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
self.short_description = short_description
|
||||
self.short_description: str = short_description
|
||||
|
||||
self._id_attrs = (self.short_description,)
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2023
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represent a Telegram bots name."""
|
||||
from typing import ClassVar
|
||||
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
|
||||
class BotName(TelegramObject):
|
||||
"""This object represents the bot's name.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`name` is equal.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): The bot's name.
|
||||
|
||||
Attributes:
|
||||
name (:obj:`str`): The bot's name.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("name",)
|
||||
|
||||
def __init__(self, name: str, *, api_kwargs: JSONDict = None):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
self.name: str = name
|
||||
|
||||
self._id_attrs = (self.name,)
|
||||
|
||||
self._freeze()
|
||||
|
||||
MAX_LENGTH: ClassVar[int] = constants.BotNameLimit.MAX_NAME_LENGTH
|
||||
""":const:`telegram.constants.BotNameLimit.MAX_NAME_LENGTH`"""
|
||||
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -54,6 +54,9 @@ class ChatInviteLink(TelegramObject):
|
||||
is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked.
|
||||
expire_date (:class:`datetime.datetime`, optional): Date when the link will expire or
|
||||
has been expired.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
member_limit (:obj:`int`, optional): Maximum number of users that can be members of the
|
||||
chat simultaneously after joining the chat via this invite link;
|
||||
:tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`-
|
||||
@@ -78,6 +81,9 @@ class ChatInviteLink(TelegramObject):
|
||||
is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked.
|
||||
expire_date (:class:`datetime.datetime`): Optional. Date when the link will expire or
|
||||
has been expired.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
member_limit (:obj:`int`): Optional. Maximum number of users that can be members
|
||||
of the chat simultaneously after joining the chat via this invite link;
|
||||
:tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`-
|
||||
@@ -152,7 +158,10 @@ class ChatInviteLink(TelegramObject):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["creator"] = User.de_json(data.get("creator"), bot)
|
||||
data["expire_date"] = from_timestamp(data.get("expire_date", None))
|
||||
data["expire_date"] = from_timestamp(data.get("expire_date", None), tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -24,7 +24,7 @@ from telegram._chat import Chat
|
||||
from telegram._chatinvitelink import ChatInviteLink
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
|
||||
@@ -41,7 +41,7 @@ class ChatJoinRequest(TelegramObject):
|
||||
Note:
|
||||
* Since Bot API 5.5, bots are allowed to contact users who sent a join request to a chat
|
||||
where the bot is an administrator with the
|
||||
:attr:`~telegram.ChatMemberAdministrator.can_invite_users` administrator right – even
|
||||
:attr:`~telegram.ChatMemberAdministrator.can_invite_users` administrator right - even
|
||||
if the user never interacted with the bot before.
|
||||
* Telegram does not guarantee that :attr:`from_user.id <from_user>` coincides with the
|
||||
``chat_id`` of the user. Please use :attr:`user_chat_id` to contact the user in
|
||||
@@ -56,6 +56,9 @@ class ChatJoinRequest(TelegramObject):
|
||||
chat (:class:`telegram.Chat`): Chat to which the request was sent.
|
||||
from_user (:class:`telegram.User`): User that sent the join request.
|
||||
date (:class:`datetime.datetime`): Date the request was sent.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
user_chat_id (:obj:`int`): Identifier of a private chat with the user who sent the join
|
||||
request. This number may have more than 32 significant bits and some programming
|
||||
languages may have difficulty/silent defects in interpreting it. But it has at most 52
|
||||
@@ -73,6 +76,9 @@ class ChatJoinRequest(TelegramObject):
|
||||
chat (:class:`telegram.Chat`): Chat to which the request was sent.
|
||||
from_user (:class:`telegram.User`): User that sent the join request.
|
||||
date (:class:`datetime.datetime`): Date the request was sent.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
user_chat_id (:obj:`int`): Identifier of a private chat with the user who sent the join
|
||||
request. This number may have more than 32 significant bits and some programming
|
||||
languages may have difficulty/silent defects in interpreting it. But it has at most 52
|
||||
@@ -124,9 +130,12 @@ class ChatJoinRequest(TelegramObject):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["date"] = from_timestamp(data.get("date", None))
|
||||
data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
|
||||
data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+17
-2
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, ClassVar, Dict, Optional, Type
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -125,7 +125,10 @@ class ChatMember(TelegramObject):
|
||||
|
||||
data["user"] = User.de_json(data.get("user"), bot)
|
||||
if "until_date" in data:
|
||||
data["until_date"] = from_timestamp(data["until_date"])
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["until_date"] = from_timestamp(data["until_date"], tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -386,6 +389,9 @@ class ChatMemberRestricted(ChatMember):
|
||||
.. versionadded:: 20.0
|
||||
until_date (:class:`datetime.datetime`): Date when restrictions
|
||||
will be lifted for this user.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios.
|
||||
|
||||
.. versionadded:: 20.1
|
||||
@@ -438,6 +444,9 @@ class ChatMemberRestricted(ChatMember):
|
||||
.. versionadded:: 20.0
|
||||
until_date (:class:`datetime.datetime`): Date when restrictions
|
||||
will be lifted for this user.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios.
|
||||
|
||||
.. versionadded:: 20.1
|
||||
@@ -565,6 +574,9 @@ class ChatMemberBanned(ChatMember):
|
||||
until_date (:class:`datetime.datetime`): Date when restrictions
|
||||
will be lifted for this user.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
|
||||
Attributes:
|
||||
status (:obj:`str`): The member's status in the chat,
|
||||
always :tg-const:`telegram.ChatMember.BANNED`.
|
||||
@@ -572,6 +584,9 @@ class ChatMemberBanned(ChatMember):
|
||||
until_date (:class:`datetime.datetime`): Date when restrictions
|
||||
will be lifted for this user.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("until_date",)
|
||||
|
||||
@@ -25,7 +25,7 @@ from telegram._chatinvitelink import ChatInviteLink
|
||||
from telegram._chatmember import ChatMember
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -52,20 +52,34 @@ class ChatMemberUpdated(TelegramObject):
|
||||
from_user (:class:`telegram.User`): Performer of the action, which resulted in the change.
|
||||
date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to
|
||||
:class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member.
|
||||
new_chat_member (:class:`telegram.ChatMember`): New information about the chat member.
|
||||
invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link, which was used
|
||||
by the user to join the chat. For joining by invite link events only.
|
||||
via_chat_folder_invite_link (:obj:`bool`, optional): :obj:`True`, if the user joined the
|
||||
chat via a chat folder invite link
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
Attributes:
|
||||
chat (:class:`telegram.Chat`): Chat the user belongs to.
|
||||
from_user (:class:`telegram.User`): Performer of the action, which resulted in the change.
|
||||
date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to
|
||||
:class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member.
|
||||
new_chat_member (:class:`telegram.ChatMember`): New information about the chat member.
|
||||
invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link, which was used
|
||||
by the user to join the chat. For joining by invite link events only.
|
||||
via_chat_folder_invite_link (:obj:`bool`): Optional. :obj:`True`, if the user joined the
|
||||
chat via a chat folder invite link
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
"""
|
||||
|
||||
@@ -76,6 +90,7 @@ class ChatMemberUpdated(TelegramObject):
|
||||
"old_chat_member",
|
||||
"new_chat_member",
|
||||
"invite_link",
|
||||
"via_chat_folder_invite_link",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -86,6 +101,7 @@ class ChatMemberUpdated(TelegramObject):
|
||||
old_chat_member: ChatMember,
|
||||
new_chat_member: ChatMember,
|
||||
invite_link: ChatInviteLink = None,
|
||||
via_chat_folder_invite_link: bool = None,
|
||||
*,
|
||||
api_kwargs: JSONDict = None,
|
||||
):
|
||||
@@ -96,6 +112,7 @@ class ChatMemberUpdated(TelegramObject):
|
||||
self.date: datetime.datetime = date
|
||||
self.old_chat_member: ChatMember = old_chat_member
|
||||
self.new_chat_member: ChatMember = new_chat_member
|
||||
self.via_chat_folder_invite_link: Optional[bool] = via_chat_folder_invite_link
|
||||
|
||||
# Optionals
|
||||
self.invite_link: Optional[ChatInviteLink] = invite_link
|
||||
@@ -118,9 +135,12 @@ class ChatMemberUpdated(TelegramObject):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["date"] = from_timestamp(data.get("date"))
|
||||
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
|
||||
data["old_chat_member"] = ChatMember.de_json(data.get("old_chat_member"), bot)
|
||||
data["new_chat_member"] = ChatMember.de_json(data.get("new_chat_member"), bot)
|
||||
data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot)
|
||||
|
||||
@@ -93,7 +93,7 @@ class _BaseThumbedMedium(_BaseMedium):
|
||||
deprecated_arg_name="thumb",
|
||||
new_arg_name="thumbnail",
|
||||
bot_api_version="6.6",
|
||||
stacklevel=4,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram InputFile."""
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
from typing import IO, Optional, Union
|
||||
from uuid import uuid4
|
||||
@@ -27,7 +26,6 @@ from telegram._utils.files import load_file
|
||||
from telegram._utils.types import FieldTuple
|
||||
|
||||
_DEFAULT_MIME_TYPE = "application/octet-stream"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InputFile:
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram Game."""
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from telegram._files.animation import Animation
|
||||
@@ -158,9 +157,6 @@ class Game(TelegramObject):
|
||||
if not self.text:
|
||||
raise RuntimeError("This Game has no 'text'.")
|
||||
|
||||
# Is it a narrow build, if so we don't need to convert
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
return self.text[entity.offset : entity.offset + entity.length]
|
||||
entity_text = self.text.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, ClassVar, Optional, Union
|
||||
from telegram import constants
|
||||
from telegram._games.callbackgame import CallbackGame
|
||||
from telegram._loginurl import LoginUrl
|
||||
from telegram._switchinlinequerychosenchat import SwitchInlineQueryChosenChat
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._webappinfo import WebAppInfo
|
||||
@@ -111,6 +112,10 @@ class InlineKeyboardButton(TelegramObject):
|
||||
in inline mode when they are currently in a private chat with it. Especially useful
|
||||
when combined with ``switch_pm*`` actions - in this case the user will be automatically
|
||||
returned to the chat they switched from, skipping the chat selection screen.
|
||||
|
||||
Tip:
|
||||
This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
|
||||
but gives no control over which chats can be selected.
|
||||
switch_inline_query_current_chat (:obj:`str`, optional): If set, pressing the button will
|
||||
insert the bot's username and the specified inline query in the current chat's input
|
||||
field. Can be empty, in which case only the bot's username will be inserted. This
|
||||
@@ -122,6 +127,20 @@ class InlineKeyboardButton(TelegramObject):
|
||||
pay (:obj:`bool`, optional): Specify :obj:`True`, to send a Pay button. This type of button
|
||||
**must** always be the **first** button in the first row and can only be used in
|
||||
invoice messages.
|
||||
switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`, optional):
|
||||
If set, pressing the button will prompt the user to select one of their chats of the
|
||||
specified type, open that chat and insert the bot's username and the specified inline
|
||||
query in the input field.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
Tip:
|
||||
This is similar to :paramref:`switch_inline_query`, but gives more control on
|
||||
which chats can be selected.
|
||||
|
||||
Caution:
|
||||
The PTB team has discovered that this field works correctly only if your Telegram
|
||||
client is released after April 20th 2023.
|
||||
|
||||
Attributes:
|
||||
text (:obj:`str`): Label text on the button.
|
||||
@@ -154,6 +173,10 @@ class InlineKeyboardButton(TelegramObject):
|
||||
in inline mode when they are currently in a private chat with it. Especially useful
|
||||
when combined with ``switch_pm*`` actions - in this case the user will be automatically
|
||||
returned to the chat they switched from, skipping the chat selection screen.
|
||||
|
||||
Tip:
|
||||
This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
|
||||
but gives no control over which chats can be selected.
|
||||
switch_inline_query_current_chat (:obj:`str`): Optional. If set, pressing the button will
|
||||
insert the bot's username and the specified inline query in the current chat's input
|
||||
field. Can be empty, in which case only the bot's username will be inserted. This
|
||||
@@ -165,7 +188,20 @@ class InlineKeyboardButton(TelegramObject):
|
||||
pay (:obj:`bool`): Optional. Specify :obj:`True`, to send a Pay button. This type of button
|
||||
**must** always be the **first** button in the first row and can only be used in
|
||||
invoice messages.
|
||||
switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`): Optional.
|
||||
If set, pressing the button will prompt the user to select one of their chats of the
|
||||
specified type, open that chat and insert the bot's username and the specified inline
|
||||
query in the input field.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
Tip:
|
||||
This is similar to :attr:`switch_inline_query`, but gives more control on
|
||||
which chats can be selected.
|
||||
|
||||
Caution:
|
||||
The PTB team has discovered that this field works correctly only if your Telegram
|
||||
client is released after April 20th 2023.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@@ -178,6 +214,7 @@ class InlineKeyboardButton(TelegramObject):
|
||||
"text",
|
||||
"login_url",
|
||||
"web_app",
|
||||
"switch_inline_query_chosen_chat",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -191,6 +228,7 @@ class InlineKeyboardButton(TelegramObject):
|
||||
pay: bool = None,
|
||||
login_url: LoginUrl = None,
|
||||
web_app: WebAppInfo = None,
|
||||
switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat = None,
|
||||
*,
|
||||
api_kwargs: JSONDict = None,
|
||||
):
|
||||
@@ -207,6 +245,9 @@ class InlineKeyboardButton(TelegramObject):
|
||||
self.callback_game: Optional[CallbackGame] = callback_game
|
||||
self.pay: Optional[bool] = pay
|
||||
self.web_app: Optional[WebAppInfo] = web_app
|
||||
self.switch_inline_query_chosen_chat: Optional[
|
||||
SwitchInlineQueryChosenChat
|
||||
] = switch_inline_query_chosen_chat
|
||||
self._id_attrs = ()
|
||||
self._set_id_attrs()
|
||||
|
||||
@@ -236,6 +277,9 @@ class InlineKeyboardButton(TelegramObject):
|
||||
data["login_url"] = LoginUrl.de_json(data.get("login_url"), bot)
|
||||
data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot)
|
||||
data["callback_game"] = CallbackGame.de_json(data.get("callback_game"), bot)
|
||||
data["switch_inline_query_chosen_chat"] = SwitchInlineQueryChosenChat.de_json(
|
||||
data.get("switch_inline_query_chosen_chat"), bot
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Callable, ClassVar, Optional, Sequence, Union
|
||||
|
||||
from telegram import constants
|
||||
from telegram._files.location import Location
|
||||
from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
@@ -146,6 +147,7 @@ class InlineQuery(TelegramObject):
|
||||
next_offset: str = None,
|
||||
switch_pm_text: str = None,
|
||||
switch_pm_parameter: str = None,
|
||||
button: InlineQueryResultsButton = None,
|
||||
*,
|
||||
current_offset: str = None,
|
||||
auto_pagination: bool = False,
|
||||
@@ -192,6 +194,7 @@ class InlineQuery(TelegramObject):
|
||||
next_offset=next_offset,
|
||||
switch_pm_text=switch_pm_text,
|
||||
switch_pm_parameter=switch_pm_parameter,
|
||||
button=button,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2023
|
||||
# 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/].
|
||||
# pylint: disable=redefined-builtin
|
||||
"""This module contains the class that represent a Telegram InlineQueryResultsButton."""
|
||||
|
||||
from typing import TYPE_CHECKING, ClassVar, Optional
|
||||
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._webappinfo import WebAppInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
|
||||
class InlineQueryResultsButton(TelegramObject):
|
||||
"""This object represents a button to be shown above inline query results. You **must** use
|
||||
exactly one of the optional fields.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`text`, :attr:`web_app` and :attr:`start_parameter` are equal.
|
||||
|
||||
Args:
|
||||
text (:obj:`str`): Label text on the button.
|
||||
web_app (:class:`telegram.WebAppInfo`, optional): Description of the
|
||||
`Web App <https://core.telegram.org/bots/webapps>`_ that will be launched when the
|
||||
user presses the button. The Web App will be able to switch back to the inline mode
|
||||
using the method
|
||||
`switchInlineQuery <https://core.telegram.org/bots/webapps#initializing-web-apps>`_
|
||||
inside the Web App.
|
||||
start_parameter (:obj:`str`, optional): Deep-linking parameter for the
|
||||
:guilabel:`/start` message sent to the bot when user presses the switch button.
|
||||
:tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`-
|
||||
:tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters,
|
||||
only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
|
||||
|
||||
Example:
|
||||
An inline bot that sends YouTube videos can ask the user to connect the bot to
|
||||
their YouTube account to adapt search results accordingly. To do this, it displays
|
||||
a 'Connect your YouTube account' button above the results, or even before showing
|
||||
any. The user presses the button, switches to a private chat with the bot and, in
|
||||
doing so, passes a start parameter that instructs the bot to return an OAuth link.
|
||||
Once done, the bot can offer a switch_inline button so that the user can easily
|
||||
return to the chat where they wanted to use the bot's inline capabilities.
|
||||
|
||||
Attributes:
|
||||
text (:obj:`str`): Label text on the button.
|
||||
web_app (:class:`telegram.WebAppInfo`): Optional. Description of the
|
||||
`Web App <https://core.telegram.org/bots/webapps>`_ that will be launched when the
|
||||
user presses the button. The Web App will be able to switch back to the inline mode
|
||||
using the method ``web_app_switch_inline_query`` inside the Web App.
|
||||
start_parameter (:obj:`str`): Optional. Deep-linking parameter for the
|
||||
:guilabel:`/start` message sent to the bot when user presses the switch button.
|
||||
:tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`-
|
||||
:tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters,
|
||||
only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("text", "web_app", "start_parameter")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
web_app: WebAppInfo = None,
|
||||
start_parameter: str = None,
|
||||
*,
|
||||
api_kwargs: JSONDict = None,
|
||||
):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
|
||||
# Required
|
||||
self.text: str = text
|
||||
|
||||
# Optional
|
||||
self.web_app: Optional[WebAppInfo] = web_app
|
||||
self.start_parameter: Optional[str] = start_parameter
|
||||
|
||||
self._id_attrs = (self.text, self.web_app, self.start_parameter)
|
||||
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineQueryResultsButton"]:
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
MIN_START_PARAMETER_LENGTH: ClassVar[
|
||||
int
|
||||
] = constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH
|
||||
""":const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`"""
|
||||
MAX_START_PARAMETER_LENGTH: ClassVar[
|
||||
int
|
||||
] = constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH
|
||||
""":const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`"""
|
||||
@@ -93,8 +93,7 @@ class MenuButton(TelegramObject):
|
||||
|
||||
if cls is MenuButton and data.get("type") in _class_mapping:
|
||||
return _class_mapping[data.pop("type")].de_json(data, bot=bot)
|
||||
out = super().de_json(data=data, bot=bot)
|
||||
return out
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
COMMANDS: ClassVar[str] = constants.MenuButtonType.COMMANDS
|
||||
""":const:`telegram.constants.MenuButtonType.COMMANDS`"""
|
||||
|
||||
+175
-156
@@ -19,7 +19,6 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram Message."""
|
||||
import datetime
|
||||
import sys
|
||||
from html import escape
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
@@ -57,9 +56,10 @@ from telegram._shared import ChatShared, UserShared
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram._videochat import (
|
||||
VideoChatEnded,
|
||||
VideoChatParticipantsInvited,
|
||||
@@ -70,6 +70,7 @@ from telegram._webappdata import WebAppData
|
||||
from telegram._writeaccessallowed import WriteAccessAllowed
|
||||
from telegram.constants import MessageAttachmentType, ParseMode
|
||||
from telegram.helpers import escape_markdown
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import (
|
||||
@@ -122,6 +123,9 @@ class Message(TelegramObject):
|
||||
sent on behalf of a chat.
|
||||
date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to
|
||||
:class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
chat (:class:`telegram.Chat`): Conversation the message belongs to.
|
||||
forward_from (:class:`telegram.User`, optional): For forwarded messages, sender of
|
||||
the original message.
|
||||
@@ -133,6 +137,9 @@ class Message(TelegramObject):
|
||||
users who disallow adding a link to their account in forwarded messages.
|
||||
forward_date (:class:`datetime.datetime`, optional): For forwarded messages, date the
|
||||
original message was sent in Unix time. Converted to :class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
is_automatic_forward (:obj:`bool`, optional): :obj:`True`, if the message is a channel
|
||||
post that was automatically forwarded to the connected discussion group.
|
||||
|
||||
@@ -142,6 +149,9 @@ class Message(TelegramObject):
|
||||
``reply_to_message`` fields even if it itself is a reply.
|
||||
edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix
|
||||
time. Converted to :class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be
|
||||
forwarded.
|
||||
|
||||
@@ -339,6 +349,9 @@ class Message(TelegramObject):
|
||||
sent on behalf of a chat.
|
||||
date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to
|
||||
:class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
chat (:class:`telegram.Chat`): Conversation the message belongs to.
|
||||
forward_from (:class:`telegram.User`): Optional. For forwarded messages, sender of the
|
||||
original message.
|
||||
@@ -348,6 +361,9 @@ class Message(TelegramObject):
|
||||
the original message in the channel.
|
||||
forward_date (:class:`datetime.datetime`): Optional. For forwarded messages, date the
|
||||
original message was sent in Unix time. Converted to :class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
is_automatic_forward (:obj:`bool`): Optional. :obj:`True`, if the message is a channel
|
||||
post that was automatically forwarded to the connected discussion group.
|
||||
|
||||
@@ -357,6 +373,9 @@ class Message(TelegramObject):
|
||||
``reply_to_message`` fields even if it itself is a reply.
|
||||
edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited in Unix
|
||||
time. Converted to :class:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be
|
||||
forwarded.
|
||||
|
||||
@@ -561,8 +580,13 @@ class Message(TelegramObject):
|
||||
|
||||
.. versionadded:: 20.1
|
||||
|
||||
.. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored
|
||||
by this function. Instead, the supplied replacement for the emoji will be used.
|
||||
.. |custom_emoji_formatting_note| replace:: Custom emoji entities will be ignored by this
|
||||
function. Instead, the supplied replacement for the emoji will be used.
|
||||
|
||||
.. |custom_emoji_md1_deprecation| replace:: Since custom emoji entities are not supported by
|
||||
:attr:`~telegram.constants.ParseMode.MARKDOWN`, this method will raise a
|
||||
:exc:`ValueError` in future versions instead of falling back to the supplied replacement
|
||||
for the emoji.
|
||||
"""
|
||||
|
||||
# fmt: on
|
||||
@@ -827,14 +851,20 @@ class Message(TelegramObject):
|
||||
def link(self) -> Optional[str]:
|
||||
""":obj:`str`: Convenience property. If the chat of the message is not
|
||||
a private chat or normal group, returns a t.me link of the message.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
For messages that are replies or part of a forum topic, the link now points
|
||||
to the corresponding thread view.
|
||||
"""
|
||||
if self.chat.type not in [Chat.PRIVATE, Chat.GROUP]:
|
||||
if self.chat.username:
|
||||
to_link = self.chat.username
|
||||
else:
|
||||
# Get rid of leading -100 for supergroups
|
||||
to_link = f"c/{str(self.chat.id)[4:]}"
|
||||
return f"https://t.me/{to_link}/{self.message_id}"
|
||||
# the else block gets rid of leading -100 for supergroups:
|
||||
to_link = self.chat.username if self.chat.username else f"c/{str(self.chat.id)[4:]}"
|
||||
baselink = f"https://t.me/{to_link}/{self.message_id}"
|
||||
|
||||
# adds the thread for topics and replies
|
||||
if (self.is_topic_message and self.message_thread_id) or self.reply_to_message:
|
||||
baselink = f"{baselink}?thread={self.message_thread_id}"
|
||||
return baselink
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@@ -845,17 +875,20 @@ class Message(TelegramObject):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["from_user"] = User.de_json(data.pop("from", None), bot)
|
||||
data["sender_chat"] = Chat.de_json(data.get("sender_chat"), bot)
|
||||
data["date"] = from_timestamp(data["date"])
|
||||
data["date"] = from_timestamp(data["date"], tzinfo=loc_tzinfo)
|
||||
data["chat"] = Chat.de_json(data.get("chat"), bot)
|
||||
data["entities"] = MessageEntity.de_list(data.get("entities"), bot)
|
||||
data["caption_entities"] = MessageEntity.de_list(data.get("caption_entities"), bot)
|
||||
data["forward_from"] = User.de_json(data.get("forward_from"), bot)
|
||||
data["forward_from_chat"] = Chat.de_json(data.get("forward_from_chat"), bot)
|
||||
data["forward_date"] = from_timestamp(data.get("forward_date"))
|
||||
data["forward_date"] = from_timestamp(data.get("forward_date"), tzinfo=loc_tzinfo)
|
||||
data["reply_to_message"] = Message.de_json(data.get("reply_to_message"), bot)
|
||||
data["edit_date"] = from_timestamp(data.get("edit_date"))
|
||||
data["edit_date"] = from_timestamp(data.get("edit_date"), tzinfo=loc_tzinfo)
|
||||
data["audio"] = Audio.de_json(data.get("audio"), bot)
|
||||
data["document"] = Document.de_json(data.get("document"), bot)
|
||||
data["animation"] = Animation.de_json(data.get("animation"), bot)
|
||||
@@ -3137,10 +3170,6 @@ class Message(TelegramObject):
|
||||
if not self.text:
|
||||
raise RuntimeError("This Message has no 'text'.")
|
||||
|
||||
# Is it a narrow build, if so we don't need to convert
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
return self.text[entity.offset : entity.offset + entity.length]
|
||||
|
||||
entity_text = self.text.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
return entity_text.decode("utf-16-le")
|
||||
@@ -3167,10 +3196,6 @@ class Message(TelegramObject):
|
||||
if not self.caption:
|
||||
raise RuntimeError("This Message has no 'caption'.")
|
||||
|
||||
# Is it a narrow build, if so we don't need to convert
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
return self.caption[entity.offset : entity.offset + entity.length]
|
||||
|
||||
entity_text = self.caption.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
return entity_text.decode("utf-16-le")
|
||||
@@ -3247,8 +3272,7 @@ class Message(TelegramObject):
|
||||
if message_text is None:
|
||||
return None
|
||||
|
||||
if sys.maxunicode != 0xFFFF:
|
||||
message_text = message_text.encode("utf-16-le") # type: ignore
|
||||
message_text = message_text.encode("utf-16-le") # type: ignore
|
||||
|
||||
html_text = ""
|
||||
last_offset = 0
|
||||
@@ -3268,78 +3292,70 @@ class Message(TelegramObject):
|
||||
parsed_entities.extend(list(nested_entities.keys()))
|
||||
|
||||
orig_text = text
|
||||
text = escape(text)
|
||||
escaped_text = escape(text)
|
||||
|
||||
if nested_entities:
|
||||
text = Message._parse_html(
|
||||
escaped_text = Message._parse_html(
|
||||
orig_text, nested_entities, urled=urled, offset=entity.offset
|
||||
)
|
||||
|
||||
if entity.type == MessageEntity.TEXT_LINK:
|
||||
insert = f'<a href="{entity.url}">{text}</a>'
|
||||
insert = f'<a href="{entity.url}">{escaped_text}</a>'
|
||||
elif entity.type == MessageEntity.TEXT_MENTION and entity.user:
|
||||
insert = f'<a href="tg://user?id={entity.user.id}">{text}</a>'
|
||||
insert = f'<a href="tg://user?id={entity.user.id}">{escaped_text}</a>'
|
||||
elif entity.type == MessageEntity.URL and urled:
|
||||
insert = f'<a href="{text}">{text}</a>'
|
||||
insert = f'<a href="{escaped_text}">{escaped_text}</a>'
|
||||
elif entity.type == MessageEntity.BOLD:
|
||||
insert = f"<b>{text}</b>"
|
||||
insert = f"<b>{escaped_text}</b>"
|
||||
elif entity.type == MessageEntity.ITALIC:
|
||||
insert = f"<i>{text}</i>"
|
||||
insert = f"<i>{escaped_text}</i>"
|
||||
elif entity.type == MessageEntity.CODE:
|
||||
insert = f"<code>{text}</code>"
|
||||
insert = f"<code>{escaped_text}</code>"
|
||||
elif entity.type == MessageEntity.PRE:
|
||||
if entity.language:
|
||||
insert = f'<pre><code class="{entity.language}">{text}</code></pre>'
|
||||
insert = (
|
||||
f'<pre><code class="{entity.language}">{escaped_text}</code></pre>'
|
||||
)
|
||||
else:
|
||||
insert = f"<pre>{text}</pre>"
|
||||
insert = f"<pre>{escaped_text}</pre>"
|
||||
elif entity.type == MessageEntity.UNDERLINE:
|
||||
insert = f"<u>{text}</u>"
|
||||
insert = f"<u>{escaped_text}</u>"
|
||||
elif entity.type == MessageEntity.STRIKETHROUGH:
|
||||
insert = f"<s>{text}</s>"
|
||||
insert = f"<s>{escaped_text}</s>"
|
||||
elif entity.type == MessageEntity.SPOILER:
|
||||
insert = f'<span class="tg-spoiler">{text}</span>'
|
||||
insert = f'<span class="tg-spoiler">{escaped_text}</span>'
|
||||
elif entity.type == MessageEntity.CUSTOM_EMOJI:
|
||||
insert = (
|
||||
f'<tg-emoji emoji-id="{entity.custom_emoji_id}">{escaped_text}</tg-emoji>'
|
||||
)
|
||||
else:
|
||||
insert = text
|
||||
insert = escaped_text
|
||||
|
||||
if offset == 0:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
html_text += (
|
||||
escape(message_text[last_offset : entity.offset - offset]) + insert
|
||||
)
|
||||
else:
|
||||
html_text += (
|
||||
escape(
|
||||
message_text[ # type: ignore
|
||||
last_offset * 2 : (entity.offset - offset) * 2
|
||||
].decode("utf-16-le")
|
||||
)
|
||||
+ insert
|
||||
)
|
||||
else:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
html_text += message_text[last_offset : entity.offset - offset] + insert
|
||||
else:
|
||||
html_text += (
|
||||
html_text += (
|
||||
escape(
|
||||
message_text[ # type: ignore
|
||||
last_offset * 2 : (entity.offset - offset) * 2
|
||||
].decode("utf-16-le")
|
||||
+ insert
|
||||
)
|
||||
+ insert
|
||||
)
|
||||
else:
|
||||
html_text += (
|
||||
message_text[ # type: ignore
|
||||
last_offset * 2 : (entity.offset - offset) * 2
|
||||
].decode("utf-16-le")
|
||||
+ insert
|
||||
)
|
||||
|
||||
last_offset = entity.offset - offset + entity.length
|
||||
|
||||
if offset == 0:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
html_text += escape(message_text[last_offset:])
|
||||
else:
|
||||
html_text += escape(
|
||||
message_text[last_offset * 2 :].decode("utf-16-le") # type: ignore
|
||||
)
|
||||
html_text += escape(
|
||||
message_text[last_offset * 2 :].decode("utf-16-le") # type: ignore
|
||||
)
|
||||
else:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
html_text += message_text[last_offset:]
|
||||
else:
|
||||
html_text += message_text[last_offset * 2 :].decode("utf-16-le") # type: ignore
|
||||
html_text += message_text[last_offset * 2 :].decode("utf-16-le") # type: ignore
|
||||
|
||||
return html_text
|
||||
|
||||
@@ -3350,12 +3366,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as HTML in
|
||||
the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as HTML.
|
||||
|
||||
@@ -3369,12 +3385,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as HTML.
|
||||
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as HTML.
|
||||
|
||||
@@ -3389,12 +3405,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
HTML in the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as HTML.
|
||||
"""
|
||||
@@ -3408,12 +3424,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as HTML.
|
||||
"""
|
||||
@@ -3432,8 +3448,7 @@ class Message(TelegramObject):
|
||||
if message_text is None:
|
||||
return None
|
||||
|
||||
if sys.maxunicode != 0xFFFF:
|
||||
message_text = message_text.encode("utf-16-le") # type: ignore
|
||||
message_text = message_text.encode("utf-16-le") # type: ignore
|
||||
|
||||
markdown_text = ""
|
||||
last_offset = 0
|
||||
@@ -3452,8 +3467,7 @@ class Message(TelegramObject):
|
||||
}
|
||||
parsed_entities.extend(list(nested_entities.keys()))
|
||||
|
||||
orig_text = text
|
||||
text = escape_markdown(text, version=version)
|
||||
escaped_text = escape_markdown(text, version=version)
|
||||
|
||||
if nested_entities:
|
||||
if version < 2:
|
||||
@@ -3461,8 +3475,8 @@ class Message(TelegramObject):
|
||||
"Nested entities are not supported for Markdown version 1"
|
||||
)
|
||||
|
||||
text = Message._parse_markdown(
|
||||
orig_text,
|
||||
escaped_text = Message._parse_markdown(
|
||||
text,
|
||||
nested_entities,
|
||||
urled=urled,
|
||||
offset=entity.offset,
|
||||
@@ -3477,105 +3491,98 @@ class Message(TelegramObject):
|
||||
url = escape_markdown(
|
||||
entity.url, version=version, entity_type=MessageEntity.TEXT_LINK
|
||||
)
|
||||
insert = f"[{text}]({url})"
|
||||
insert = f"[{escaped_text}]({url})"
|
||||
elif entity.type == MessageEntity.TEXT_MENTION and entity.user:
|
||||
insert = f"[{text}](tg://user?id={entity.user.id})"
|
||||
insert = f"[{escaped_text}](tg://user?id={entity.user.id})"
|
||||
elif entity.type == MessageEntity.URL and urled:
|
||||
if version == 1:
|
||||
link = orig_text
|
||||
else:
|
||||
link = text
|
||||
insert = f"[{link}]({orig_text})"
|
||||
link = text if version == 1 else escaped_text
|
||||
insert = f"[{link}]({text})"
|
||||
elif entity.type == MessageEntity.BOLD:
|
||||
insert = f"*{text}*"
|
||||
insert = f"*{escaped_text}*"
|
||||
elif entity.type == MessageEntity.ITALIC:
|
||||
insert = f"_{text}_"
|
||||
insert = f"_{escaped_text}_"
|
||||
elif entity.type == MessageEntity.CODE:
|
||||
# Monospace needs special escaping. Also can't have entities nested within
|
||||
insert = f"`{escape_markdown(orig_text, version, MessageEntity.CODE)}`"
|
||||
insert = f"`{escape_markdown(text, version, MessageEntity.CODE)}`"
|
||||
|
||||
elif entity.type == MessageEntity.PRE:
|
||||
# Monospace needs special escaping. Also can't have entities nested within
|
||||
code = escape_markdown(
|
||||
orig_text, version=version, entity_type=MessageEntity.PRE
|
||||
)
|
||||
code = escape_markdown(text, version=version, entity_type=MessageEntity.PRE)
|
||||
if entity.language:
|
||||
prefix = f"```{entity.language}\n"
|
||||
elif code.startswith("\\"):
|
||||
prefix = "```"
|
||||
else:
|
||||
if code.startswith("\\"):
|
||||
prefix = "```"
|
||||
else:
|
||||
prefix = "```\n"
|
||||
prefix = "```\n"
|
||||
insert = f"{prefix}{code}```"
|
||||
elif entity.type == MessageEntity.UNDERLINE:
|
||||
if version == 1:
|
||||
raise ValueError(
|
||||
"Underline entities are not supported for Markdown version 1"
|
||||
)
|
||||
insert = f"__{text}__"
|
||||
insert = f"__{escaped_text}__"
|
||||
elif entity.type == MessageEntity.STRIKETHROUGH:
|
||||
if version == 1:
|
||||
raise ValueError(
|
||||
"Strikethrough entities are not supported for Markdown version 1"
|
||||
)
|
||||
insert = f"~{text}~"
|
||||
insert = f"~{escaped_text}~"
|
||||
elif entity.type == MessageEntity.SPOILER:
|
||||
if version == 1:
|
||||
raise ValueError(
|
||||
"Spoiler entities are not supported for Markdown version 1"
|
||||
)
|
||||
insert = f"||{text}||"
|
||||
insert = f"||{escaped_text}||"
|
||||
elif entity.type == MessageEntity.CUSTOM_EMOJI:
|
||||
if version == 1:
|
||||
# this ensures compatibility to previous PTB versions
|
||||
insert = escaped_text
|
||||
warn(
|
||||
"Custom emoji entities are not supported for Markdown version 1. "
|
||||
"Future version of PTB will raise a ValueError instead of falling "
|
||||
"back to the alternative standard emoji.",
|
||||
stacklevel=3,
|
||||
category=PTBDeprecationWarning,
|
||||
)
|
||||
else:
|
||||
# This should never be needed because ids are numeric but the documentation
|
||||
# specifically mentions it so here we are
|
||||
custom_emoji_id = escape_markdown(
|
||||
entity.custom_emoji_id,
|
||||
version=version,
|
||||
entity_type=MessageEntity.CUSTOM_EMOJI,
|
||||
)
|
||||
insert = f""
|
||||
else:
|
||||
insert = text
|
||||
insert = escaped_text
|
||||
|
||||
if offset == 0:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
markdown_text += (
|
||||
escape_markdown(
|
||||
message_text[last_offset : entity.offset - offset], version=version
|
||||
)
|
||||
+ insert
|
||||
)
|
||||
else:
|
||||
markdown_text += (
|
||||
escape_markdown(
|
||||
message_text[ # type: ignore
|
||||
last_offset * 2 : (entity.offset - offset) * 2
|
||||
].decode("utf-16-le"),
|
||||
version=version,
|
||||
)
|
||||
+ insert
|
||||
)
|
||||
else:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
markdown_text += (
|
||||
message_text[last_offset : entity.offset - offset] + insert
|
||||
)
|
||||
else:
|
||||
markdown_text += (
|
||||
markdown_text += (
|
||||
escape_markdown(
|
||||
message_text[ # type: ignore
|
||||
last_offset * 2 : (entity.offset - offset) * 2
|
||||
].decode("utf-16-le")
|
||||
+ insert
|
||||
].decode("utf-16-le"),
|
||||
version=version,
|
||||
)
|
||||
+ insert
|
||||
)
|
||||
else:
|
||||
markdown_text += (
|
||||
message_text[ # type: ignore
|
||||
last_offset * 2 : (entity.offset - offset) * 2
|
||||
].decode("utf-16-le")
|
||||
+ insert
|
||||
)
|
||||
|
||||
last_offset = entity.offset - offset + entity.length
|
||||
|
||||
if offset == 0:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
markdown_text += escape_markdown(message_text[last_offset:], version=version)
|
||||
else:
|
||||
markdown_text += escape_markdown(
|
||||
message_text[last_offset * 2 :].decode("utf-16-le"), # type: ignore
|
||||
version=version,
|
||||
)
|
||||
markdown_text += escape_markdown(
|
||||
message_text[last_offset * 2 :].decode("utf-16-le"), # type: ignore
|
||||
version=version,
|
||||
)
|
||||
else:
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
markdown_text += message_text[last_offset:]
|
||||
else:
|
||||
markdown_text += message_text[last_offset * 2 :].decode( # type: ignore
|
||||
"utf-16-le"
|
||||
)
|
||||
markdown_text += message_text[last_offset * 2 :].decode("utf-16-le") # type: ignore
|
||||
|
||||
return markdown_text
|
||||
|
||||
@@ -3594,6 +3601,9 @@ class Message(TelegramObject):
|
||||
|
||||
* |custom_emoji_formatting_note|
|
||||
|
||||
.. deprecated:: 20.3
|
||||
|custom_emoji_md1_deprecation|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as Markdown.
|
||||
|
||||
@@ -3612,12 +3622,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as Markdown
|
||||
in the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as Markdown.
|
||||
"""
|
||||
@@ -3638,6 +3648,9 @@ class Message(TelegramObject):
|
||||
|
||||
* |custom_emoji_formatting_note|
|
||||
|
||||
.. deprecated:: 20.3
|
||||
|custom_emoji_md1_deprecation|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as Markdown.
|
||||
|
||||
@@ -3656,12 +3669,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as Markdown.
|
||||
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as Markdown.
|
||||
"""
|
||||
@@ -3682,6 +3695,9 @@ class Message(TelegramObject):
|
||||
|
||||
* |custom_emoji_formatting_note|
|
||||
|
||||
.. deprecated:: 20.3
|
||||
|custom_emoji_md1_deprecation|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as Markdown.
|
||||
|
||||
@@ -3700,12 +3716,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
Markdown in the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as Markdown.
|
||||
"""
|
||||
@@ -3728,6 +3744,9 @@ class Message(TelegramObject):
|
||||
|
||||
* |custom_emoji_formatting_note|
|
||||
|
||||
.. deprecated:: 20.3
|
||||
|custom_emoji_md1_deprecation|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as Markdown.
|
||||
|
||||
@@ -3746,12 +3765,12 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entities are now supported.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as Markdown.
|
||||
"""
|
||||
|
||||
@@ -180,7 +180,7 @@ class PassportElementErrorFiles(PassportElementError):
|
||||
with self._unfrozen():
|
||||
self.file_hashes: str = file_hashes
|
||||
|
||||
self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes)
|
||||
self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes))
|
||||
|
||||
|
||||
class PassportElementErrorFrontSide(PassportElementError):
|
||||
@@ -362,7 +362,7 @@ class PassportElementErrorTranslationFiles(PassportElementError):
|
||||
with self._unfrozen():
|
||||
self.file_hashes: str = file_hashes
|
||||
|
||||
self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes)
|
||||
self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes))
|
||||
|
||||
|
||||
class PassportElementErrorUnspecified(PassportElementError):
|
||||
|
||||
@@ -24,7 +24,7 @@ from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import LabeledPrice # noqa
|
||||
from telegram import LabeledPrice
|
||||
|
||||
|
||||
class ShippingOption(TelegramObject):
|
||||
|
||||
+11
-6
@@ -18,7 +18,6 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram Poll."""
|
||||
import datetime
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from telegram import constants
|
||||
@@ -27,7 +26,7 @@ from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -174,6 +173,9 @@ class Poll(TelegramObject):
|
||||
close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the
|
||||
poll will be automatically closed. Converted to :obj:`datetime.datetime`.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
|
||||
Attributes:
|
||||
id (:obj:`str`): Unique poll identifier.
|
||||
question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`-
|
||||
@@ -207,6 +209,9 @@ class Poll(TelegramObject):
|
||||
close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be
|
||||
automatically closed.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@@ -272,9 +277,12 @@ class Poll(TelegramObject):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["options"] = [PollOption.de_json(option, bot) for option in data["options"]]
|
||||
data["explanation_entities"] = MessageEntity.de_list(data.get("explanation_entities"), bot)
|
||||
data["close_date"] = from_timestamp(data.get("close_date"))
|
||||
data["close_date"] = from_timestamp(data.get("close_date"), tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -300,9 +308,6 @@ class Poll(TelegramObject):
|
||||
if not self.explanation:
|
||||
raise RuntimeError("This Poll has no 'explanation'.")
|
||||
|
||||
# Is it a narrow build, if so we don't need to convert
|
||||
if sys.maxunicode == 0xFFFF:
|
||||
return self.explanation[entity.offset : entity.offset + entity.length]
|
||||
entity_text = self.explanation.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2023
|
||||
# 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
|
||||
"""This module contains a class that represents a Telegram SwitchInlineQueryChosenChat."""
|
||||
from typing import Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
|
||||
class SwitchInlineQueryChosenChat(TelegramObject):
|
||||
"""
|
||||
This object represents an inline button that switches the current user to inline mode in a
|
||||
chosen chat, with an optional default inline query.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`query`, :attr:`allow_user_chats`, :attr:`allow_bot_chats`,
|
||||
:attr:`allow_group_chats`, and :attr:`allow_channel_chats` are equal.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
Caution:
|
||||
The PTB team has discovered that you must pass at least one of
|
||||
:paramref:`allow_user_chats`, :paramref:`allow_bot_chats`, :paramref:`allow_group_chats`,
|
||||
or :paramref:`allow_channel_chats` to Telegram. Otherwise, an error will be raised.
|
||||
|
||||
Args:
|
||||
query (:obj:`str`, optional): The default inline query to be inserted in the input field.
|
||||
If left empty, only the bot's username will be inserted.
|
||||
allow_user_chats (:obj:`bool`, optional): Pass :obj:`True`, if private chats with users
|
||||
can be chosen.
|
||||
allow_bot_chats (:obj:`bool`, optional): Pass :obj:`True`, if private chats with bots can
|
||||
be chosen.
|
||||
allow_group_chats (:obj:`bool`, optional): Pass :obj:`True`, if group and supergroup chats
|
||||
can be chosen.
|
||||
allow_channel_chats (:obj:`bool`, optional): Pass :obj:`True`, if channel chats can be
|
||||
chosen.
|
||||
|
||||
Attributes:
|
||||
query (:obj:`str`): Optional. The default inline query to be inserted in the input field.
|
||||
If left empty, only the bot's username will be inserted.
|
||||
allow_user_chats (:obj:`bool`): Optional. :obj:`True`, if private chats with users can be
|
||||
chosen.
|
||||
allow_bot_chats (:obj:`bool`): Optional. :obj:`True`, if private chats with bots can be
|
||||
chosen.
|
||||
allow_group_chats (:obj:`bool`): Optional. :obj:`True`, if group and supergroup chats can
|
||||
be chosen.
|
||||
allow_channel_chats (:obj:`bool`): Optional. :obj:`True`, if channel chats can be chosen.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"query",
|
||||
"allow_user_chats",
|
||||
"allow_bot_chats",
|
||||
"allow_group_chats",
|
||||
"allow_channel_chats",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query: str = None,
|
||||
allow_user_chats: bool = None,
|
||||
allow_bot_chats: bool = None,
|
||||
allow_group_chats: bool = None,
|
||||
allow_channel_chats: bool = None,
|
||||
*,
|
||||
api_kwargs: JSONDict = None,
|
||||
):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
# Optional
|
||||
self.query: Optional[str] = query
|
||||
self.allow_user_chats: Optional[bool] = allow_user_chats
|
||||
self.allow_bot_chats: Optional[bool] = allow_bot_chats
|
||||
self.allow_group_chats: Optional[bool] = allow_group_chats
|
||||
self.allow_channel_chats: Optional[bool] = allow_channel_chats
|
||||
|
||||
self._id_attrs = (
|
||||
self.query,
|
||||
self.allow_user_chats,
|
||||
self.allow_bot_chats,
|
||||
self.allow_group_chats,
|
||||
self.allow_channel_chats,
|
||||
)
|
||||
|
||||
self._freeze()
|
||||
@@ -58,6 +58,12 @@ class TelegramObject:
|
||||
The :mod:`pickle` and :func:`~copy.deepcopy` behavior of objects of this type are defined by
|
||||
:meth:`__getstate__`, :meth:`__setstate__` and :meth:`__deepcopy__`.
|
||||
|
||||
Tip:
|
||||
Objects of this type can be serialized via Python's :mod:`pickle` module and pickled
|
||||
objects from one version of PTB are usually loadable in future versions. However, we can
|
||||
not guarantee that this compatibility will always be provided. At least a manual one-time
|
||||
conversion of the data may be needed on major updates of the library.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* Removed argument and attribute ``bot`` for several subclasses. Use
|
||||
@@ -281,7 +287,7 @@ class TelegramObject:
|
||||
|
||||
# Make sure that we have a `_bot` attribute. This is necessary, since __getstate__ omits
|
||||
# this as Bots are not pickable.
|
||||
setattr(self, "_bot", None)
|
||||
self._bot = None
|
||||
|
||||
# get api_kwargs first because we may need to add entries to it (see try-except below)
|
||||
api_kwargs = cast(Dict[str, object], state.pop("api_kwargs", {}))
|
||||
@@ -299,7 +305,7 @@ class TelegramObject:
|
||||
# and then set the rest as MappingProxyType attribute. Converting to MappingProxyType
|
||||
# is necessary, since __getstate__ converts it to a dict as MPT is not pickable.
|
||||
self._apply_api_kwargs(api_kwargs)
|
||||
setattr(self, "api_kwargs", MappingProxyType(api_kwargs))
|
||||
self.api_kwargs = MappingProxyType(api_kwargs)
|
||||
|
||||
# Apply freezing if necessary
|
||||
# we .get(…) the setting for backwards compatibility with objects that were pickled
|
||||
@@ -328,7 +334,7 @@ class TelegramObject:
|
||||
result = cls.__new__(cls) # create a new instance
|
||||
memodict[id(self)] = result # save the id of the object in the dict
|
||||
|
||||
setattr(result, "_frozen", False) # unfreeze the new object for setting the attributes
|
||||
result._frozen = False # unfreeze the new object for setting the attributes
|
||||
|
||||
# now we set the attributes in the deepcopied object
|
||||
for k in self._get_attrs_names(include_private=True):
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot, Chat, User # noqa
|
||||
from telegram import Bot, Chat, User
|
||||
|
||||
|
||||
class Update(TelegramObject):
|
||||
@@ -46,7 +46,7 @@ class Update(TelegramObject):
|
||||
Note:
|
||||
At most one of the optional parameters can be present in any given update.
|
||||
|
||||
.. seealso:: :wiki:`Your First Bot <Extensions-–-Your-first-Bot>`
|
||||
.. seealso:: :wiki:`Your First Bot <Extensions---Your-first-Bot>`
|
||||
|
||||
Args:
|
||||
update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a
|
||||
|
||||
@@ -29,7 +29,10 @@ Warning:
|
||||
"""
|
||||
import datetime as dtm # skipcq: PYL-W0406
|
||||
import time
|
||||
from typing import Optional, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
# pytz is only available if it was installed as dependency of APScheduler, so we make a little
|
||||
# workaround here
|
||||
@@ -162,7 +165,10 @@ def to_timestamp(
|
||||
)
|
||||
|
||||
|
||||
def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optional[dtm.datetime]:
|
||||
def from_timestamp(
|
||||
unixtime: Optional[int],
|
||||
tzinfo: Optional[dtm.tzinfo] = None,
|
||||
) -> Optional[dtm.datetime]:
|
||||
"""
|
||||
Converts an (integer) unix timestamp to a timezone aware datetime object.
|
||||
:obj:`None` s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`).
|
||||
@@ -170,7 +176,8 @@ def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optiona
|
||||
Args:
|
||||
unixtime (:obj:`int`): Integer POSIX timestamp.
|
||||
tzinfo (:obj:`datetime.tzinfo`, optional): The timezone to which the timestamp is to be
|
||||
converted to. Defaults to UTC.
|
||||
converted to. Defaults to :obj:`None`, in which case the returned datetime object will
|
||||
be timezone aware and in UTC.
|
||||
|
||||
Returns:
|
||||
Timezone aware equivalent :obj:`datetime.datetime` value if :paramref:`unixtime` is not
|
||||
@@ -179,9 +186,19 @@ def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optiona
|
||||
if unixtime is None:
|
||||
return None
|
||||
|
||||
if tzinfo is not None:
|
||||
return dtm.datetime.fromtimestamp(unixtime, tz=tzinfo)
|
||||
return dtm.datetime.utcfromtimestamp(unixtime)
|
||||
return dtm.datetime.fromtimestamp(unixtime, tz=UTC if tzinfo is None else tzinfo)
|
||||
|
||||
|
||||
def extract_tzinfo_from_defaults(bot: "Bot") -> Union[dtm.tzinfo, None]:
|
||||
"""
|
||||
Extracts the timezone info from the default values of the bot.
|
||||
If the bot has no default values, :obj:`None` is returned.
|
||||
"""
|
||||
# We don't use `ininstance(bot, ExtBot)` here so that this works
|
||||
# in `python-telegram-bot-raw` as well
|
||||
if hasattr(bot, "defaults") and bot.defaults:
|
||||
return bot.defaults.tzinfo
|
||||
return None
|
||||
|
||||
|
||||
def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2023
|
||||
# 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 helper functions related to logging.
|
||||
|
||||
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 logging
|
||||
|
||||
|
||||
def get_logger(file_name: str, class_name: str = None) -> logging.Logger:
|
||||
"""Returns a logger with an appropriate name.
|
||||
Use as follows::
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
If for example `__name__` is `telegram.ext._updater`, the logger will be named
|
||||
`telegram.ext.Updater`. If `class_name` is passed, this will result in
|
||||
`telegram.ext.<class_name>`. Useful e.g. for CamelCase class names.
|
||||
|
||||
If the file name points to a utils module, the logger name will simply be `telegram(.ext)`.
|
||||
|
||||
Returns:
|
||||
:class:`logging.Logger`: The logger.
|
||||
"""
|
||||
parts = file_name.split("_")
|
||||
if parts[1].startswith("utils") and class_name is None:
|
||||
name = parts[0].rstrip(".")
|
||||
else:
|
||||
name = f"{parts[0]}{class_name or parts[1].capitalize()}"
|
||||
return logging.getLogger(name)
|
||||
@@ -60,7 +60,7 @@ DVInput = Union["DefaultValue[DVValueType]", DVValueType, "DefaultValue[None]"]
|
||||
as ``Union[DefaultValue[type], type, DefaultValue[None]]``."""
|
||||
|
||||
RT = TypeVar("RT")
|
||||
SCT = Union[RT, Collection[RT]]
|
||||
SCT = Union[RT, Collection[RT]] # pylint: disable=invalid-name
|
||||
"""Single instance or collection of instances."""
|
||||
|
||||
ReplyMarkup = Union[
|
||||
|
||||
@@ -23,8 +23,7 @@ inside warnings.py.
|
||||
|
||||
.. versionadded:: 20.2
|
||||
"""
|
||||
import functools
|
||||
from typing import Any
|
||||
from typing import Any, Callable, Type
|
||||
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
@@ -38,7 +37,8 @@ def warn_about_deprecated_arg_return_new_arg(
|
||||
deprecated_arg_name: str,
|
||||
new_arg_name: str,
|
||||
bot_api_version: str,
|
||||
stacklevel: int = 3,
|
||||
stacklevel: int = 2,
|
||||
warn_callback: Callable[[str, Type[Warning], int], None] = warn,
|
||||
) -> Any:
|
||||
"""A helper function for the transition in API when argument is renamed.
|
||||
|
||||
@@ -58,11 +58,11 @@ def warn_about_deprecated_arg_return_new_arg(
|
||||
)
|
||||
|
||||
if deprecated_arg:
|
||||
warn(
|
||||
warn_callback(
|
||||
f"Bot API {bot_api_version} renamed the argument '{deprecated_arg_name}' to "
|
||||
f"'{new_arg_name}'.",
|
||||
PTBDeprecationWarning,
|
||||
stacklevel=stacklevel,
|
||||
stacklevel + 1,
|
||||
)
|
||||
return deprecated_arg
|
||||
|
||||
@@ -73,7 +73,7 @@ def warn_about_deprecated_attr_in_property(
|
||||
deprecated_attr_name: str,
|
||||
new_attr_name: str,
|
||||
bot_api_version: str,
|
||||
stacklevel: int = 3,
|
||||
stacklevel: int = 2,
|
||||
) -> None:
|
||||
"""A helper function for the transition in API when attribute is renamed. Call from properties.
|
||||
|
||||
@@ -83,16 +83,25 @@ def warn_about_deprecated_attr_in_property(
|
||||
f"Bot API {bot_api_version} renamed the attribute '{deprecated_attr_name}' to "
|
||||
f"'{new_attr_name}'.",
|
||||
PTBDeprecationWarning,
|
||||
stacklevel=stacklevel,
|
||||
stacklevel=stacklevel + 1,
|
||||
)
|
||||
|
||||
|
||||
warn_about_thumb_return_thumbnail = functools.partial(
|
||||
warn_about_deprecated_arg_return_new_arg,
|
||||
deprecated_arg_name="thumb",
|
||||
new_arg_name="thumbnail",
|
||||
bot_api_version="6.6",
|
||||
)
|
||||
"""A helper function to warn about using a deprecated 'thumb' argument and return it or the new
|
||||
'thumbnail' argument, introduced in API 6.6.
|
||||
"""
|
||||
def warn_about_thumb_return_thumbnail(
|
||||
deprecated_arg: Any,
|
||||
new_arg: Any,
|
||||
stacklevel: int = 2,
|
||||
warn_callback: Callable[[str, Type[Warning], int], None] = warn,
|
||||
) -> Any:
|
||||
"""A helper function to warn about using a deprecated 'thumb' argument and return it or the
|
||||
new 'thumbnail' argument, introduced in API 6.6.
|
||||
"""
|
||||
return warn_about_deprecated_arg_return_new_arg(
|
||||
deprecated_arg=deprecated_arg,
|
||||
new_arg=new_arg,
|
||||
warn_callback=warn_callback,
|
||||
deprecated_arg_name="thumb",
|
||||
new_arg_name="thumbnail",
|
||||
bot_api_version="6.6",
|
||||
stacklevel=stacklevel + 1,
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ class Version(NamedTuple):
|
||||
return version
|
||||
|
||||
|
||||
__version_info__ = Version(major=20, minor=2, micro=0, releaselevel="final", serial=0)
|
||||
__version_info__ = Version(major=20, minor=3, micro=0, releaselevel="final", serial=0)
|
||||
__version__ = str(__version_info__)
|
||||
|
||||
# # SETUP.PY MARKER
|
||||
|
||||
+11
-2
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Optional, Sequence, Tuple
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -149,10 +149,16 @@ class VideoChatScheduled(TelegramObject):
|
||||
Args:
|
||||
start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the video
|
||||
chat is supposed to be started by a chat administrator
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
Attributes:
|
||||
start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the video
|
||||
chat is supposed to be started by a chat administrator
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("start_date",)
|
||||
@@ -178,6 +184,9 @@ class VideoChatScheduled(TelegramObject):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["start_date"] = from_timestamp(data["start_date"])
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["start_date"] = from_timestamp(data["start_date"], tzinfo=loc_tzinfo)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
+28
-11
@@ -21,7 +21,7 @@ from typing import TYPE_CHECKING, Optional, Sequence, Tuple
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.datetime import from_timestamp
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -49,8 +49,11 @@ class WebhookInfo(TelegramObject):
|
||||
webhook certificate checks.
|
||||
pending_update_count (:obj:`int`): Number of updates awaiting delivery.
|
||||
ip_address (:obj:`str`, optional): Currently used webhook IP address.
|
||||
last_error_date (:obj:`int`, optional): Unix time for the most recent error that happened
|
||||
when trying to deliver an update via webhook.
|
||||
last_error_date (:class:`datetime.datetime`): Optional. Datetime for the most recent
|
||||
error that happened when trying to deliver an update via webhook.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
last_error_message (:obj:`str`, optional): Error message in human-readable format for the
|
||||
most recent error that happened when trying to deliver an update via webhook.
|
||||
max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS
|
||||
@@ -62,18 +65,25 @@ class WebhookInfo(TelegramObject):
|
||||
.. versionchanged:: 20.0
|
||||
|sequenceclassargs|
|
||||
|
||||
last_synchronization_error_date (:obj:`int`, optional): Unix time of the most recent error
|
||||
that happened when trying to synchronize available updates with Telegram datacenters.
|
||||
last_synchronization_error_date (:class:`datetime.datetime`, optional): Datetime of the
|
||||
most recent error that happened when trying to synchronize available updates with
|
||||
Telegram datacenters.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
Attributes:
|
||||
url (:obj:`str`): Webhook URL, may be empty if webhook is not set up.
|
||||
has_custom_certificate (:obj:`bool`): :obj:`True`, if a custom certificate was provided for
|
||||
webhook certificate checks.
|
||||
pending_update_count (:obj:`int`): Number of updates awaiting delivery.
|
||||
ip_address (:obj:`str`): Optional. Currently used webhook IP address.
|
||||
last_error_date (:obj:`int`): Optional. Unix time for the most recent error that happened
|
||||
when trying to deliver an update via webhook.
|
||||
last_error_date (:class:`datetime.datetime`): Optional. Datetime for the most recent
|
||||
error that happened when trying to deliver an update via webhook.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
last_error_message (:obj:`str`): Optional. Error message in human-readable format for the
|
||||
most recent error that happened when trying to deliver an update via webhook.
|
||||
max_connections (:obj:`int`): Optional. Maximum allowed number of simultaneous HTTPS
|
||||
@@ -86,10 +96,14 @@ class WebhookInfo(TelegramObject):
|
||||
|
||||
* |tupleclassattrs|
|
||||
* |alwaystuple|
|
||||
last_synchronization_error_date (:obj:`int`): Optional. Unix time of the most recent error
|
||||
that happened when trying to synchronize available updates with Telegram datacenters.
|
||||
last_synchronization_error_date (:class:`datetime.datetime`, optional): Datetime of the
|
||||
most recent error that happened when trying to synchronize available updates with
|
||||
Telegram datacenters.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@@ -154,9 +168,12 @@ class WebhookInfo(TelegramObject):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["last_error_date"] = from_timestamp(data.get("last_error_date"))
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
|
||||
data["last_error_date"] = from_timestamp(data.get("last_error_date"), tzinfo=loc_tzinfo)
|
||||
data["last_synchronization_error_date"] = from_timestamp(
|
||||
data.get("last_synchronization_error_date")
|
||||
data.get("last_synchronization_error_date"), tzinfo=loc_tzinfo
|
||||
)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -17,21 +17,35 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains objects related to the write access allowed service message."""
|
||||
from typing import Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
|
||||
class WriteAccessAllowed(TelegramObject):
|
||||
"""
|
||||
This object represents a service message about a user allowing a bot added to the attachment
|
||||
menu to write messages. Currently holds no information.
|
||||
This object represents a service message about a user allowing a bot to write messages after
|
||||
adding the bot to the attachment menu or launching a Web App from a link.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
web_app_name (:obj:`str`, optional): Name of the Web App which was launched from a link.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
Attributes:
|
||||
web_app_name (:obj:`str`): Optional. Name of the Web App which was launched from a link.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
__slots__ = ("web_app_name",)
|
||||
|
||||
def __init__(self, *, api_kwargs: JSONDict = None):
|
||||
def __init__(self, web_app_name: str = None, *, api_kwargs: JSONDict = None):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
self.web_app_name: Optional[str] = web_app_name
|
||||
|
||||
self._freeze()
|
||||
|
||||
+48
-3
@@ -37,6 +37,7 @@ __all__ = [
|
||||
"BotCommandLimit",
|
||||
"BotCommandScopeType",
|
||||
"BotDescriptionLimit",
|
||||
"BotNameLimit",
|
||||
"CallbackQueryLimit",
|
||||
"ChatAction",
|
||||
"ChatID",
|
||||
@@ -57,6 +58,7 @@ __all__ = [
|
||||
"InlineKeyboardMarkupLimit",
|
||||
"InlineQueryLimit",
|
||||
"InlineQueryResultLimit",
|
||||
"InlineQueryResultsButtonLimit",
|
||||
"InlineQueryResultType",
|
||||
"InputMediaType",
|
||||
"InvoiceLimit",
|
||||
@@ -114,7 +116,7 @@ class _BotAPIVersion(NamedTuple):
|
||||
#: :data:`telegram.__bot_api_version_info__`.
|
||||
#:
|
||||
#: .. versionadded:: 20.0
|
||||
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=6)
|
||||
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=7)
|
||||
#: :obj:`str`: Telegram Bot API
|
||||
#: version supported by this version of `python-telegram-bot`. Also available as
|
||||
#: :data:`telegram.__bot_api_version__`.
|
||||
@@ -209,6 +211,21 @@ class BotDescriptionLimit(IntEnum):
|
||||
"""
|
||||
|
||||
|
||||
class BotNameLimit(IntEnum):
|
||||
"""This enum contains limitations for the methods :meth:`telegram.Bot.set_my_name`.
|
||||
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
MAX_NAME_LENGTH = 64
|
||||
""":obj:`int`: Maximum length for the parameter :paramref:`~telegram.Bot.set_my_name.name` of
|
||||
:meth:`telegram.Bot.set_my_name`
|
||||
"""
|
||||
|
||||
|
||||
class CallbackQueryLimit(IntEnum):
|
||||
"""This enum contains limitations for :class:`telegram.CallbackQuery`/
|
||||
:meth:`telegram.Bot.answer_callback_query`. The enum members of this enumeration are instances
|
||||
@@ -735,11 +752,19 @@ class InlineQueryLimit(IntEnum):
|
||||
MIN_SWITCH_PM_TEXT_LENGTH = 1
|
||||
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of
|
||||
:meth:`telegram.Bot.answer_inline_query`."""
|
||||
:meth:`telegram.Bot.answer_inline_query`.
|
||||
|
||||
.. deprecated:: 20.3
|
||||
Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`.
|
||||
"""
|
||||
MAX_SWITCH_PM_TEXT_LENGTH = 64
|
||||
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of
|
||||
:meth:`telegram.Bot.answer_inline_query`."""
|
||||
:meth:`telegram.Bot.answer_inline_query`.
|
||||
|
||||
.. deprecated:: 20.3
|
||||
Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`.
|
||||
"""
|
||||
|
||||
|
||||
class InlineQueryResultLimit(IntEnum):
|
||||
@@ -763,6 +788,26 @@ class InlineQueryResultLimit(IntEnum):
|
||||
"""
|
||||
|
||||
|
||||
class InlineQueryResultsButtonLimit(IntEnum):
|
||||
"""This enum contains limitations for :class:`telegram.InlineQueryResultsButton`.
|
||||
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
MIN_START_PARAMETER_LENGTH = InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH
|
||||
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of
|
||||
:meth:`telegram.InlineQueryResultsButton`."""
|
||||
|
||||
MAX_START_PARAMETER_LENGTH = InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH
|
||||
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of
|
||||
:meth:`telegram.InlineQueryResultsButton`."""
|
||||
|
||||
|
||||
class InlineQueryResultType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.InlineQueryResult`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
+7
-5
@@ -48,17 +48,19 @@ def _lstrip_str(in_s: str, lstr: str) -> str:
|
||||
:obj:`str`: The stripped string.
|
||||
|
||||
"""
|
||||
if in_s.startswith(lstr):
|
||||
res = in_s[len(lstr) :]
|
||||
else:
|
||||
res = in_s
|
||||
return res
|
||||
return in_s[len(lstr) :] if in_s.startswith(lstr) else in_s
|
||||
|
||||
|
||||
class TelegramError(Exception):
|
||||
"""
|
||||
Base class for Telegram errors.
|
||||
|
||||
Tip:
|
||||
Objects of this type can be serialized via Python's :mod:`pickle` module and pickled
|
||||
objects from one version of PTB are usually loadable in future versions. However, we can
|
||||
not guarantee that this compatibility will always be provided. At least a manual one-time
|
||||
conversion of the data may be needed on major updates of the library.
|
||||
|
||||
.. seealso:: :wiki:`Exceptions, Warnings and Logging <Exceptions%2C-Warnings-and-Logging>`
|
||||
"""
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ library.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, AsyncIterator, Callable, Coroutine, Dict, List, Optional, Union
|
||||
|
||||
@@ -32,6 +31,7 @@ try:
|
||||
except ImportError:
|
||||
AIO_LIMITER_AVAILABLE = False
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.error import RetryAfter
|
||||
from telegram.ext._baseratelimiter import BaseRateLimiter
|
||||
@@ -48,6 +48,9 @@ else:
|
||||
yield None
|
||||
|
||||
|
||||
_LOGGER = get_logger(__name__, class_name="AIORateLimiter")
|
||||
|
||||
|
||||
class AIORateLimiter(BaseRateLimiter[int]):
|
||||
"""
|
||||
Implementation of :class:`~telegram.ext.BaseRateLimiter` using the library
|
||||
@@ -118,7 +121,6 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
"_group_limiters",
|
||||
"_group_max_rate",
|
||||
"_group_time_period",
|
||||
"_logger",
|
||||
"_max_retries",
|
||||
"_retry_after_event",
|
||||
)
|
||||
@@ -152,7 +154,6 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
|
||||
self._group_limiters: Dict[Union[str, int], AsyncLimiter] = {}
|
||||
self._max_retries: int = max_retries
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._retry_after_event = asyncio.Event()
|
||||
self._retry_after_event.set()
|
||||
|
||||
@@ -203,7 +204,7 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
return await callback(*args, **kwargs)
|
||||
|
||||
# mypy doesn't understand that the last run of the for loop raises an exception
|
||||
async def process_request( # type: ignore[return]
|
||||
async def process_request(
|
||||
self,
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, List[JSONDict]]]],
|
||||
args: Any,
|
||||
@@ -232,10 +233,8 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
chat = True
|
||||
|
||||
# In case user passes integer chat id as string
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
chat_id = int(chat_id)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if (isinstance(chat_id, int) and chat_id < 0) or isinstance(chat_id, str):
|
||||
# string chat_id only works for channels and supergroups
|
||||
@@ -249,16 +248,17 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
)
|
||||
except RetryAfter as exc:
|
||||
if i == max_retries:
|
||||
self._logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Rate limit hit after maximum of %d retries", max_retries, exc_info=exc
|
||||
)
|
||||
raise exc
|
||||
|
||||
sleep = exc.retry_after + 0.1
|
||||
self._logger.info("Rate limit hit. Retrying after %f seconds", sleep)
|
||||
_LOGGER.info("Rate limit hit. Retrying after %f seconds", sleep)
|
||||
# Make sure we don't allow other requests to be processed
|
||||
self._retry_after_event.clear()
|
||||
await asyncio.sleep(sleep)
|
||||
finally:
|
||||
# Allow other requests to be processed
|
||||
self._retry_after_event.set()
|
||||
return None # type: ignore[return-value]
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the Application class."""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import platform
|
||||
import signal
|
||||
from collections import defaultdict
|
||||
@@ -52,7 +52,8 @@ from typing import (
|
||||
|
||||
from telegram._update import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DEFAULT_TRUE, DefaultValue
|
||||
from telegram._utils.types import DVType, ODVInput
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import SCT, DVType, ODVInput
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext._basepersistence import BasePersistence
|
||||
@@ -74,8 +75,9 @@ DEFAULT_GROUP: int = 0
|
||||
|
||||
_AppType = TypeVar("_AppType", bound="Application") # pylint: disable=invalid-name
|
||||
_STOP_SIGNAL = object()
|
||||
_DEFAULT_0 = DefaultValue(0)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class ApplicationHandlerStop(Exception):
|
||||
@@ -137,7 +139,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
Examples:
|
||||
:any:`Echo Bot <examples.echobot>`
|
||||
|
||||
.. seealso:: :wiki:`Your First Bot <Extensions-–-Your-first-Bot>`,
|
||||
.. seealso:: :wiki:`Your First Bot <Extensions---Your-first-Bot>`,
|
||||
:wiki:`Architecture Overview <Architecture>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
@@ -391,7 +393,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
:meth:`shutdown`
|
||||
"""
|
||||
if self._initialized:
|
||||
_logger.debug("This Application is already initialized.")
|
||||
_LOGGER.debug("This Application is already initialized.")
|
||||
return
|
||||
|
||||
await self.bot.initialize()
|
||||
@@ -441,7 +443,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
raise RuntimeError("This Application is still running!")
|
||||
|
||||
if not self._initialized:
|
||||
_logger.debug("This Application is already shut down. Returning.")
|
||||
_LOGGER.debug("This Application is already shut down. Returning.")
|
||||
return
|
||||
|
||||
await self.bot.shutdown()
|
||||
@@ -449,10 +451,10 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
await self.updater.shutdown()
|
||||
|
||||
if self.persistence:
|
||||
_logger.debug("Updating & flushing persistence before shutdown")
|
||||
_LOGGER.debug("Updating & flushing persistence before shutdown")
|
||||
await self.update_persistence()
|
||||
await self.persistence.flush()
|
||||
_logger.debug("Updated and flushed persistence")
|
||||
_LOGGER.debug("Updated and flushed persistence")
|
||||
|
||||
self._initialized = False
|
||||
|
||||
@@ -555,18 +557,18 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
# TODO: Add this once we drop py3.7
|
||||
# name=f'Application:{self.bot.id}:persistence_updater'
|
||||
)
|
||||
_logger.debug("Loop for updating persistence started")
|
||||
_LOGGER.debug("Loop for updating persistence started")
|
||||
|
||||
if self._job_queue:
|
||||
await self._job_queue.start() # type: ignore[union-attr]
|
||||
_logger.debug("JobQueue started")
|
||||
_LOGGER.debug("JobQueue started")
|
||||
|
||||
self.__update_fetcher_task = asyncio.create_task(
|
||||
self._update_fetcher(),
|
||||
# TODO: Add this once we drop py3.7
|
||||
# name=f'Application:{self.bot.id}:update_fetcher'
|
||||
)
|
||||
_logger.info("Application started")
|
||||
_LOGGER.info("Application started")
|
||||
|
||||
except Exception as exc:
|
||||
self._running = False
|
||||
@@ -599,32 +601,32 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
raise RuntimeError("This Application is not running!")
|
||||
|
||||
self._running = False
|
||||
_logger.info("Application is stopping. This might take a moment.")
|
||||
_LOGGER.info("Application is stopping. This might take a moment.")
|
||||
|
||||
# Stop listening for new updates and handle all pending ones
|
||||
await self.update_queue.put(_STOP_SIGNAL)
|
||||
_logger.debug("Waiting for update_queue to join")
|
||||
_LOGGER.debug("Waiting for update_queue to join")
|
||||
await self.update_queue.join()
|
||||
if self.__update_fetcher_task:
|
||||
await self.__update_fetcher_task
|
||||
_logger.debug("Application stopped fetching of updates.")
|
||||
_LOGGER.debug("Application stopped fetching of updates.")
|
||||
|
||||
if self._job_queue:
|
||||
_logger.debug("Waiting for running jobs to finish")
|
||||
_LOGGER.debug("Waiting for running jobs to finish")
|
||||
await self._job_queue.stop(wait=True) # type: ignore[union-attr]
|
||||
_logger.debug("JobQueue stopped")
|
||||
_LOGGER.debug("JobQueue stopped")
|
||||
|
||||
_logger.debug("Waiting for `create_task` calls to be processed")
|
||||
_LOGGER.debug("Waiting for `create_task` calls to be processed")
|
||||
await asyncio.gather(*self.__create_task_tasks, return_exceptions=True)
|
||||
|
||||
# Make sure that this is the *last* step of stopping the application!
|
||||
if self.persistence and self.__update_persistence_task:
|
||||
_logger.debug("Waiting for persistence loop to finish")
|
||||
_LOGGER.debug("Waiting for persistence loop to finish")
|
||||
self.__update_persistence_event.set()
|
||||
await self.__update_persistence_task
|
||||
self.__update_persistence_event.clear()
|
||||
|
||||
_logger.info("Application.stop() complete")
|
||||
_LOGGER.info("Application.stop() complete")
|
||||
|
||||
def run_polling(
|
||||
self,
|
||||
@@ -997,10 +999,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
self.__create_task_tasks.discard(task) # Discard from our set since we are done with it
|
||||
# We just retrieve the eventual exception so that asyncio doesn't complain in case
|
||||
# it's not retrieved somewhere else
|
||||
try:
|
||||
with contextlib.suppress(asyncio.CancelledError, asyncio.InvalidStateError):
|
||||
task.exception()
|
||||
except (asyncio.CancelledError, asyncio.InvalidStateError):
|
||||
pass
|
||||
|
||||
async def __create_task_callback(
|
||||
self,
|
||||
@@ -1026,7 +1026,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
|
||||
# Avoid infinite recursion of error handlers.
|
||||
elif is_error_handler:
|
||||
_logger.exception(
|
||||
_LOGGER.exception(
|
||||
"An error was raised and an uncaught error was raised while "
|
||||
"handling the error with an error_handler.",
|
||||
exc_info=exception,
|
||||
@@ -1046,24 +1046,33 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
async def _update_fetcher(self) -> None:
|
||||
# Continuously fetch updates from the queue. Exit only once the signal object is found.
|
||||
while True:
|
||||
update = await self.update_queue.get()
|
||||
try:
|
||||
update = await self.update_queue.get()
|
||||
|
||||
if update is _STOP_SIGNAL:
|
||||
_logger.debug("Dropping pending updates")
|
||||
while not self.update_queue.empty():
|
||||
if update is _STOP_SIGNAL:
|
||||
_LOGGER.debug("Dropping pending updates")
|
||||
while not self.update_queue.empty():
|
||||
self.update_queue.task_done()
|
||||
|
||||
# For the _STOP_SIGNAL
|
||||
self.update_queue.task_done()
|
||||
return
|
||||
|
||||
# For the _STOP_SIGNAL
|
||||
self.update_queue.task_done()
|
||||
return
|
||||
_LOGGER.debug("Processing update %s", update)
|
||||
|
||||
_logger.debug("Processing update %s", update)
|
||||
|
||||
if self._concurrent_updates:
|
||||
# We don't await the below because it has to be run concurrently
|
||||
self.create_task(self.__process_update_wrapper(update), update=update)
|
||||
else:
|
||||
await self.__process_update_wrapper(update)
|
||||
if self._concurrent_updates:
|
||||
# We don't await the below because it has to be run concurrently
|
||||
self.create_task(self.__process_update_wrapper(update), update=update)
|
||||
else:
|
||||
await self.__process_update_wrapper(update)
|
||||
except asyncio.CancelledError:
|
||||
# This may happen if the application is manually run via application.start() and
|
||||
# then a KeyboardInterrupt is sent. We must prevent this loop to die since
|
||||
# application.stop() will wait for it's clean shutdown.
|
||||
_LOGGER.warning(
|
||||
"Fetching updates got a asyncio.CancelledError. Ignoring as this task may only"
|
||||
"be closed via `Application.stop`."
|
||||
)
|
||||
|
||||
async def __process_update_wrapper(self, update: object) -> None:
|
||||
async with self._concurrent_updates_sem:
|
||||
@@ -1117,13 +1126,13 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
|
||||
# Stop processing with any other handler.
|
||||
except ApplicationHandlerStop:
|
||||
_logger.debug("Stopping further handlers due to ApplicationHandlerStop")
|
||||
_LOGGER.debug("Stopping further handlers due to ApplicationHandlerStop")
|
||||
break
|
||||
|
||||
# Dispatch any error.
|
||||
except Exception as exc:
|
||||
if await self.process_error(update=update, error=exc):
|
||||
_logger.debug("Error handler stopped further handlers.")
|
||||
_LOGGER.debug("Error handler stopped further handlers.")
|
||||
break
|
||||
|
||||
if any_blocking:
|
||||
@@ -1200,7 +1209,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
Union[List[BaseHandler[Any, CCT]], Tuple[BaseHandler[Any, CCT]]],
|
||||
Dict[int, Union[List[BaseHandler[Any, CCT]], Tuple[BaseHandler[Any, CCT]]]],
|
||||
],
|
||||
group: Union[int, DefaultValue[int]] = DefaultValue(0),
|
||||
group: Union[int, DefaultValue[int]] = _DEFAULT_0,
|
||||
) -> None:
|
||||
"""Registers multiple handlers at once. The order of the handlers in the passed
|
||||
sequence(s) matters. See :meth:`add_handler` for details.
|
||||
@@ -1373,6 +1382,36 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
if job.user_id:
|
||||
self._user_ids_to_be_updated_in_persistence.add(job.user_id)
|
||||
|
||||
def mark_data_for_update_persistence(
|
||||
self, chat_ids: SCT[int] = None, user_ids: SCT[int] = None
|
||||
) -> None:
|
||||
"""Mark entries of :attr:`chat_data` and :attr:`user_data` to be updated on the next
|
||||
run of :meth:`update_persistence`.
|
||||
|
||||
Tip:
|
||||
Use this method sparingly. If you have to use this method, it likely means that you
|
||||
access and modify ``context.application.chat/user_data[some_id]`` within a callback.
|
||||
Note that for data which should be available globally in all handler callbacks
|
||||
independent of the chat/user, it is recommended to use :attr:`bot_data` instead.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
Args:
|
||||
chat_ids (:obj:`int` | Collection[:obj:`int`], optional): Chat IDs to mark.
|
||||
user_ids (:obj:`int` | Collection[:obj:`int`], optional): User IDs to mark.
|
||||
|
||||
"""
|
||||
if chat_ids:
|
||||
if isinstance(chat_ids, int):
|
||||
self._chat_ids_to_be_updated_in_persistence.add(chat_ids)
|
||||
else:
|
||||
self._chat_ids_to_be_updated_in_persistence.update(chat_ids)
|
||||
if user_ids:
|
||||
if isinstance(user_ids, int):
|
||||
self._user_ids_to_be_updated_in_persistence.add(user_ids)
|
||||
else:
|
||||
self._user_ids_to_be_updated_in_persistence.update(user_ids)
|
||||
|
||||
async def _persistence_updater(self) -> None:
|
||||
# Update the persistence in regular intervals. Exit only when the stop event has been set
|
||||
while not self.__update_persistence_event.is_set():
|
||||
@@ -1399,8 +1438,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
along with :attr:`~telegram.ext.ExtBot.callback_data_cache` and the conversation states of
|
||||
any persistent :class:`~telegram.ext.ConversationHandler` registered for this application.
|
||||
|
||||
For :attr:`user_data`, :attr:`chat_data`, only entries used since the last run of this
|
||||
method are updated.
|
||||
For :attr:`user_data` and :attr:`chat_data`, only those entries are updated which either
|
||||
were used or have been manually marked via :meth:`mark_data_for_update_persistence` since
|
||||
the last run of this method.
|
||||
|
||||
Tip:
|
||||
This method will be called in regular intervals by the application. There is usually
|
||||
@@ -1410,7 +1450,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
Any data is deep copied with :func:`copy.deepcopy` before handing it over to the
|
||||
persistence in order to avoid race conditions, so all persisted data must be copyable.
|
||||
|
||||
.. seealso:: :attr:`telegram.ext.BasePersistence.update_interval`.
|
||||
.. seealso:: :attr:`telegram.ext.BasePersistence.update_interval`,
|
||||
:meth:`mark_data_for_update_persistence`
|
||||
"""
|
||||
async with self.__update_persistence_lock:
|
||||
await self.__update_persistence()
|
||||
@@ -1419,7 +1460,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
if not self.persistence:
|
||||
return
|
||||
|
||||
_logger.debug("Starting next run of updating the persistence.")
|
||||
_LOGGER.debug("Starting next run of updating the persistence.")
|
||||
|
||||
coroutines: Set[Coroutine] = set()
|
||||
|
||||
@@ -1487,13 +1528,13 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
# *all* tasks will be done.
|
||||
if not new_state.done():
|
||||
if self.running:
|
||||
_logger.debug(
|
||||
_LOGGER.debug(
|
||||
"A ConversationHandlers state was not yet resolved. Updating the "
|
||||
"persistence with the current state. Will check again on next run of "
|
||||
"Application.update_persistence."
|
||||
)
|
||||
else:
|
||||
_logger.warning(
|
||||
_LOGGER.warning(
|
||||
"A ConversationHandlers state was not yet resolved. Updating the "
|
||||
"persistence with the current state."
|
||||
)
|
||||
@@ -1513,7 +1554,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
)
|
||||
|
||||
results = await asyncio.gather(*coroutines, return_exceptions=True)
|
||||
_logger.debug("Finished updating persistence.")
|
||||
_LOGGER.debug("Finished updating persistence.")
|
||||
|
||||
# dispatch any errors
|
||||
await asyncio.gather(
|
||||
@@ -1554,7 +1595,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
:meth:`process_error`. Defaults to :obj:`True`.
|
||||
"""
|
||||
if callback in self.error_handlers:
|
||||
_logger.warning("The callback is already registered as an error handler. Ignoring.")
|
||||
_LOGGER.warning("The callback is already registered as an error handler. Ignoring.")
|
||||
return
|
||||
|
||||
self.error_handlers[callback] = block
|
||||
@@ -1631,12 +1672,12 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
||||
except ApplicationHandlerStop:
|
||||
return True
|
||||
except Exception as exc:
|
||||
_logger.exception(
|
||||
_LOGGER.exception(
|
||||
"An error was raised and an uncaught error was raised while "
|
||||
"handling the error with an error_handler.",
|
||||
exc_info=exc,
|
||||
)
|
||||
return False
|
||||
|
||||
_logger.exception("No error handlers are registered, logging exception.", exc_info=error)
|
||||
_LOGGER.exception("No error handlers are registered, logging exception.", exc_info=error)
|
||||
return False
|
||||
|
||||
@@ -114,7 +114,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
* Unless a custom :class:`telegram.Bot` instance is set via :meth:`bot`, :meth:`build` will
|
||||
use :class:`telegram.ext.ExtBot` for the bot.
|
||||
|
||||
.. seealso:: :wiki:`Your First Bot <Extensions-–-Your-first-Bot>`,
|
||||
.. seealso:: :wiki:`Your First Bot <Extensions---Your-first-Bot>`,
|
||||
:wiki:`Builder Pattern <Builder-Pattern>`
|
||||
|
||||
.. _`builder pattern`: https://en.wikipedia.org/wiki/Builder_pattern
|
||||
@@ -570,12 +570,13 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
|
||||
Note:
|
||||
Users have observed stability issues with HTTP/2, which happen due to how the `h2
|
||||
library handles <https://github.com/python-hyper/h2/issues/1181>` cancellations of
|
||||
library handles <https://github.com/python-hyper/h2/issues/1181>`_ cancellations of
|
||||
keepalive connections. See `#3556 <https://github.com/python-telegram-bot/
|
||||
python-telegram-bot/issues/3556>`_ for a discussion.
|
||||
|
||||
If you want to use HTTP/2, you must install PTB with the optional requirement
|
||||
``http2``, i.e.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install python-telegram-bot[http2]
|
||||
@@ -728,7 +729,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
|
||||
Note:
|
||||
Users have observed stability issues with HTTP/2, which happen due to how the `h2
|
||||
library handles <https://github.com/python-hyper/h2/issues/1181>` cancellations of
|
||||
library handles <https://github.com/python-hyper/h2/issues/1181>`_ cancellations of
|
||||
keepalive connections. See `#3556 <https://github.com/python-telegram-bot/
|
||||
python-telegram-bot/issues/3556>`_ for a discussion.
|
||||
|
||||
|
||||
@@ -357,6 +357,11 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
||||
:attr:`~telegram.ext.Application.user_data` to a callback. Can be used to update data
|
||||
stored in :attr:`~telegram.ext.Application.user_data` from an external source.
|
||||
|
||||
Warning:
|
||||
When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method
|
||||
may be called while a handler callback is still running. This might lead to race
|
||||
conditions.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
@@ -375,6 +380,11 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
||||
:attr:`~telegram.ext.Application.chat_data` to a callback. Can be used to update data
|
||||
stored in :attr:`~telegram.ext.Application.chat_data` from an external source.
|
||||
|
||||
Warning:
|
||||
When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method
|
||||
may be called while a handler callback is still running. This might lead to race
|
||||
conditions.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
@@ -393,6 +403,11 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
||||
:attr:`~telegram.ext.Application.bot_data` to a callback. Can be used to update data stored
|
||||
in :attr:`~telegram.ext.Application.bot_data` from an external source.
|
||||
|
||||
Warning:
|
||||
When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method
|
||||
may be called while a handler callback is still running. This might lead to race
|
||||
conditions.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
@@ -41,7 +41,7 @@ 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 # noqa: F401
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
from telegram.ext._utils.types import CCT
|
||||
|
||||
_STORING_DATA_WIKI = (
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"""This module contains the ConversationHandler."""
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -38,6 +37,7 @@ from typing import (
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE, DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import DVType
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._application import ApplicationHandlerStop
|
||||
@@ -56,7 +56,7 @@ if TYPE_CHECKING:
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
_CheckUpdateType = Tuple[object, ConversationKey, BaseHandler[Update, CCT], object]
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_LOGGER = get_logger(__name__, class_name="ConversationHandler")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -102,7 +102,7 @@ class PendingState:
|
||||
|
||||
exc = self.task.exception()
|
||||
if exc:
|
||||
_logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Task function raised exception. Falling back to old state %s",
|
||||
self.old_state,
|
||||
)
|
||||
@@ -649,12 +649,12 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
try:
|
||||
effective_new_state = await new_state
|
||||
except Exception as exc:
|
||||
_logger.debug(
|
||||
_LOGGER.debug(
|
||||
"Non-blocking handler callback raised exception. Not scheduling conversation "
|
||||
"timeout.",
|
||||
exc_info=exc,
|
||||
)
|
||||
return
|
||||
return None
|
||||
return self._schedule_job(
|
||||
new_state=effective_new_state,
|
||||
application=application,
|
||||
@@ -684,7 +684,7 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
data=_ConversationTimeoutContext(conversation_key, update, application, context),
|
||||
)
|
||||
except Exception as exc:
|
||||
_logger.exception("Failed to schedule timeout.", exc_info=exc)
|
||||
_LOGGER.exception("Failed to schedule timeout.", exc_info=exc)
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def check_update(self, update: object) -> Optional[_CheckUpdateType[CCT]]:
|
||||
@@ -719,7 +719,7 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
|
||||
# Resolve futures
|
||||
if isinstance(state, PendingState):
|
||||
_logger.debug("Waiting for asyncio Task to finish ...")
|
||||
_LOGGER.debug("Waiting for asyncio Task to finish ...")
|
||||
|
||||
# check if future is finished or not
|
||||
if state.done():
|
||||
@@ -741,7 +741,7 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
return self.WAITING, key, handler_, check
|
||||
return None
|
||||
|
||||
_logger.debug("Selecting conversation %s with state %s", str(key), str(state))
|
||||
_LOGGER.debug("Selecting conversation %s with state %s", str(key), str(state))
|
||||
|
||||
handler: Optional[BaseHandler] = None
|
||||
|
||||
@@ -813,13 +813,12 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
# 3. Default values of the bot
|
||||
if handler.block is not DEFAULT_TRUE:
|
||||
block = handler.block
|
||||
elif self._block is not DEFAULT_TRUE:
|
||||
block = self._block
|
||||
elif isinstance(application.bot, ExtBot) and application.bot.defaults is not None:
|
||||
block = application.bot.defaults.block
|
||||
else:
|
||||
if self._block is not DEFAULT_TRUE:
|
||||
block = self._block
|
||||
elif isinstance(application.bot, ExtBot) and application.bot.defaults is not None:
|
||||
block = application.bot.defaults.block
|
||||
else:
|
||||
block = DefaultValue.get_value(handler.block)
|
||||
block = DefaultValue.get_value(handler.block)
|
||||
|
||||
try: # Now create task or await the callback
|
||||
if block:
|
||||
@@ -841,26 +840,25 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
if application.job_queue is None:
|
||||
warn(
|
||||
"Ignoring `conversation_timeout` because the Application has no JobQueue.",
|
||||
stacklevel=1,
|
||||
)
|
||||
elif not application.job_queue.scheduler.running:
|
||||
warn(
|
||||
"Ignoring `conversation_timeout` because the Applications JobQueue is "
|
||||
"not running.",
|
||||
stacklevel=1,
|
||||
)
|
||||
else:
|
||||
elif isinstance(new_state, asyncio.Task):
|
||||
# Add the new timeout job
|
||||
# checking if the new state is self.END is done in _schedule_job
|
||||
if isinstance(new_state, asyncio.Task):
|
||||
application.create_task(
|
||||
self._schedule_job_delayed(
|
||||
new_state, application, update, context, conversation_key
|
||||
),
|
||||
update=update,
|
||||
)
|
||||
else:
|
||||
self._schedule_job(
|
||||
application.create_task(
|
||||
self._schedule_job_delayed(
|
||||
new_state, application, update, context, conversation_key
|
||||
)
|
||||
),
|
||||
update=update,
|
||||
)
|
||||
else:
|
||||
self._schedule_job(new_state, application, update, context, conversation_key)
|
||||
|
||||
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
|
||||
self._update_state(self.END, conversation_key, handler)
|
||||
@@ -874,7 +872,7 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
if raise_dp_handler_stop:
|
||||
# Don't pass the new state here. If we're in a nested conversation, the parent is
|
||||
# expecting None as return value.
|
||||
raise ApplicationHandlerStop()
|
||||
raise ApplicationHandlerStop
|
||||
# Signals a possible parent conversation to stay in the current state
|
||||
return None
|
||||
|
||||
@@ -909,7 +907,7 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
job = cast("Job", context.job)
|
||||
ctxt = cast(_ConversationTimeoutContext, job.data)
|
||||
|
||||
_logger.debug(
|
||||
_LOGGER.debug(
|
||||
"Conversation timeout was triggered for conversation %s!", ctxt.conversation_key
|
||||
)
|
||||
|
||||
@@ -935,6 +933,7 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
warn(
|
||||
"ApplicationHandlerStop in TIMEOUT state of "
|
||||
"ConversationHandler has no effect. Ignoring.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self._update_state(self.END, ctxt.conversation_key)
|
||||
|
||||
@@ -468,12 +468,12 @@ class DictPersistence(BasePersistence[Dict[Any, Any], Dict[Any, Any], Dict[Any,
|
||||
tmp: Dict[int, Dict[object, object]] = {}
|
||||
decoded_data = json.loads(data)
|
||||
for user, user_data in decoded_data.items():
|
||||
user = int(user)
|
||||
tmp[user] = {}
|
||||
int_user_id = int(user)
|
||||
tmp[int_user_id] = {}
|
||||
for key, value in user_data.items():
|
||||
try:
|
||||
key = int(key)
|
||||
_id = int(key)
|
||||
except ValueError:
|
||||
pass
|
||||
tmp[user][key] = value
|
||||
_id = key
|
||||
tmp[int_user_id][_id] = value
|
||||
return tmp
|
||||
|
||||
+76
-31
@@ -18,7 +18,6 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram Bot with convenience extensions."""
|
||||
import warnings
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
@@ -31,6 +30,7 @@ from typing import (
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
@@ -46,6 +46,7 @@ from telegram import (
|
||||
BotCommand,
|
||||
BotCommandScope,
|
||||
BotDescription,
|
||||
BotName,
|
||||
BotShortDescription,
|
||||
CallbackQuery,
|
||||
Chat,
|
||||
@@ -60,6 +61,7 @@ from telegram import (
|
||||
ForumTopic,
|
||||
GameHighScore,
|
||||
InlineKeyboardMarkup,
|
||||
InlineQueryResultsButton,
|
||||
InputMedia,
|
||||
InputSticker,
|
||||
Location,
|
||||
@@ -85,12 +87,12 @@ from telegram import (
|
||||
)
|
||||
from telegram._utils.datetime import to_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._callbackdatacache import CallbackDataCache
|
||||
from telegram.ext._utils.types import RLARGS
|
||||
from telegram.request import BaseRequest
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from telegram.warnings import PTBUserWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import (
|
||||
@@ -156,6 +158,8 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
|
||||
__slots__ = ("_callback_data_cache", "_defaults", "_rate_limiter")
|
||||
|
||||
_LOGGER = get_logger(__name__, class_name="ExtBot")
|
||||
|
||||
# using object() would be a tiny bit safer, but a string plays better with the typing setup
|
||||
__RL_KEY = uuid4().hex
|
||||
|
||||
@@ -232,6 +236,15 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
|
||||
self._callback_data_cache = CallbackDataCache(bot=self, maxsize=maxsize)
|
||||
|
||||
@classmethod
|
||||
def _warn(
|
||||
cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0
|
||||
) -> None:
|
||||
"""We override this method to add one more level to the stacklevel, so that the warning
|
||||
points to the user's code, not to the PTB code.
|
||||
"""
|
||||
super()._warn(message=message, category=category, stacklevel=stacklevel + 2)
|
||||
|
||||
@property
|
||||
def callback_data_cache(self) -> Optional[CallbackDataCache]:
|
||||
""":class:`telegram.ext.CallbackDataCache`: Optional. The cache for
|
||||
@@ -324,7 +337,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
"connect_timeout": connect_timeout,
|
||||
"pool_timeout": pool_timeout,
|
||||
}
|
||||
self._logger.debug(
|
||||
self._LOGGER.debug(
|
||||
"Passing request through rate limiter of type %s with rate_limit_args %s",
|
||||
type(self.rate_limiter),
|
||||
rate_limit_args,
|
||||
@@ -385,10 +398,10 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
# 3)
|
||||
elif isinstance(val, InputMedia) and val.parse_mode is DEFAULT_NONE:
|
||||
# Copy object as not to edit it in-place
|
||||
val = copy(val)
|
||||
with val._unfrozen():
|
||||
val.parse_mode = self.defaults.parse_mode if self.defaults else None
|
||||
data[key] = val
|
||||
copied_val = copy(val)
|
||||
with copied_val._unfrozen():
|
||||
copied_val.parse_mode = self.defaults.parse_mode if self.defaults else None
|
||||
data[key] = copied_val
|
||||
elif key == "media" and isinstance(val, Sequence):
|
||||
# Copy objects as not to edit them in-place
|
||||
copy_list = [copy(media) for media in val]
|
||||
@@ -781,6 +794,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
next_offset: str = None,
|
||||
switch_pm_text: str = None,
|
||||
switch_pm_parameter: str = None,
|
||||
button: InlineQueryResultsButton = None,
|
||||
*,
|
||||
current_offset: str = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -803,6 +817,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
button=button,
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
)
|
||||
|
||||
@@ -3281,30 +3296,16 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
api_kwargs: JSONDict = None,
|
||||
rate_limit_args: RLARGS = None,
|
||||
) -> bool:
|
||||
# Manually issue deprecation here to get the stacklevel right
|
||||
# Suppress the warning issued by super().set_sticker_set_thumb just in case
|
||||
# the user explicitly enables deprecation warnings
|
||||
# Unfortunately this is not entirely reliable (see tests), but it's a best effort solution
|
||||
warn(
|
||||
message=(
|
||||
"Bot API 6.6 renamed the method 'setStickerSetThumb' to 'setStickerSetThumbnail', "
|
||||
"hence method 'set_sticker_set_thumb' was renamed to 'set_sticker_set_thumbnail' "
|
||||
"in PTB."
|
||||
),
|
||||
category=PTBDeprecationWarning,
|
||||
stacklevel=2,
|
||||
return await super().set_sticker_set_thumb(
|
||||
name=name,
|
||||
user_id=user_id,
|
||||
thumb=thumb,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
)
|
||||
with warnings.catch_warnings():
|
||||
return await super().set_sticker_set_thumb(
|
||||
name=name,
|
||||
user_id=user_id,
|
||||
thumb=thumb,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
)
|
||||
|
||||
async def set_webhook(
|
||||
self,
|
||||
@@ -3584,6 +3585,48 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
)
|
||||
|
||||
async def set_my_name(
|
||||
self,
|
||||
name: str = None,
|
||||
language_code: str = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
rate_limit_args: RLARGS = None,
|
||||
) -> bool:
|
||||
return await super().set_my_name(
|
||||
name=name,
|
||||
language_code=language_code,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
)
|
||||
|
||||
async def get_my_name(
|
||||
self,
|
||||
language_code: str = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
rate_limit_args: RLARGS = None,
|
||||
) -> BotName:
|
||||
return await super().get_my_name(
|
||||
language_code=language_code,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
)
|
||||
|
||||
async def set_custom_emoji_sticker_set_thumbnail(
|
||||
self,
|
||||
name: str,
|
||||
@@ -3826,3 +3869,5 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
setStickerEmojiList = set_sticker_emoji_list
|
||||
setStickerKeywords = set_sticker_keywords
|
||||
setStickerMaskPosition = set_sticker_mask_position
|
||||
setMyName = set_my_name
|
||||
getMyName = get_my_name
|
||||
|
||||
@@ -41,6 +41,9 @@ if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
|
||||
_ALL_DAYS = tuple(range(7))
|
||||
|
||||
|
||||
class JobQueue(Generic[CCT]):
|
||||
"""This class allows you to periodically perform tasks with the bot. It is a convenience
|
||||
wrapper for the APScheduler library.
|
||||
@@ -288,6 +291,18 @@ class JobQueue(Generic[CCT]):
|
||||
:attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
Defaults to :paramref:`interval`
|
||||
|
||||
Note:
|
||||
Setting :paramref:`first` to ``0``, ``datetime.datetime.now()`` or another
|
||||
value that indicates that the job should run immediately will not work due
|
||||
to how the APScheduler library works. If you want to run a job immediately,
|
||||
we recommend to use an approach along the lines of::
|
||||
|
||||
job = context.job_queue.run_repeating(callback, interval=5)
|
||||
await job.run(context.application)
|
||||
|
||||
.. seealso:: :meth:`telegram.ext.Job.run`
|
||||
|
||||
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||
:obj:`datetime.datetime` | :obj:`datetime.time`, optional):
|
||||
Latest possible time for the job to run. This parameter will be interpreted
|
||||
@@ -436,7 +451,7 @@ class JobQueue(Generic[CCT]):
|
||||
self,
|
||||
callback: JobCallback[CCT],
|
||||
time: datetime.time,
|
||||
days: Tuple[int, ...] = tuple(range(7)),
|
||||
days: Tuple[int, ...] = _ALL_DAYS,
|
||||
data: object = None,
|
||||
name: str = None,
|
||||
chat_id: int = None,
|
||||
|
||||
@@ -47,7 +47,7 @@ class MessageHandler(BaseHandler[Update, CCT]):
|
||||
operators (& for and, | for or, ~ for not). Passing :obj:`None` is a shortcut
|
||||
to passing :class:`telegram.ext.filters.ALL`.
|
||||
|
||||
.. seealso:: :wiki:`Advanced Filters <Extensions-–-Advanced-Filters>`
|
||||
.. seealso:: :wiki:`Advanced Filters <Extensions---Advanced-Filters>`
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
@@ -98,7 +98,10 @@ class _BotPickler(pickle.Pickler):
|
||||
if obj is self._bot:
|
||||
return _REPLACED_KNOWN_BOT
|
||||
if isinstance(obj, Bot):
|
||||
warn("Unknown bot instance found. Will be replaced by `None` during unpickling")
|
||||
warn(
|
||||
"Unknown bot instance found. Will be replaced by `None` during unpickling",
|
||||
stacklevel=2,
|
||||
)
|
||||
return _REPLACED_UNKNOWN_BOT
|
||||
return None # pickles as usual
|
||||
|
||||
|
||||
@@ -132,15 +132,9 @@ class PrefixHandler(BaseHandler[Update, CCT]):
|
||||
):
|
||||
super().__init__(callback=callback, block=block)
|
||||
|
||||
if isinstance(prefix, str):
|
||||
prefixes = {prefix.lower()}
|
||||
else:
|
||||
prefixes = {x.lower() for x in prefix}
|
||||
prefixes = {prefix.lower()} if isinstance(prefix, str) else {x.lower() for x in prefix}
|
||||
|
||||
if isinstance(command, str):
|
||||
commands = {command.lower()}
|
||||
else:
|
||||
commands = {x.lower() for x in command}
|
||||
commands = {command.lower()} if isinstance(command, str) else {x.lower() for x in command}
|
||||
|
||||
self.commands: FrozenSet[str] = frozenset(
|
||||
p + c for p, c in itertools.product(prefixes, commands)
|
||||
|
||||
+36
-38
@@ -18,7 +18,7 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the class Updater, which tries to make creating Telegram bots intuitive."""
|
||||
import asyncio
|
||||
import logging
|
||||
import contextlib
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
@@ -35,6 +35,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import ODVInput
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
@@ -50,6 +51,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
_UpdaterType = TypeVar("_UpdaterType", bound="Updater") # pylint: disable=invalid-name
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class Updater(AsyncContextManager["Updater"]):
|
||||
@@ -96,7 +98,6 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
|
||||
__slots__ = (
|
||||
"bot",
|
||||
"_logger",
|
||||
"update_queue",
|
||||
"_last_update_id",
|
||||
"_running",
|
||||
@@ -120,7 +121,6 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
self._httpd: Optional[WebhookServer] = None
|
||||
self.__lock = asyncio.Lock()
|
||||
self.__polling_task: Optional[asyncio.Task] = None
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
@@ -134,7 +134,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
:meth:`shutdown`
|
||||
"""
|
||||
if self._initialized:
|
||||
self._logger.debug("This Updater is already initialized.")
|
||||
_LOGGER.debug("This Updater is already initialized.")
|
||||
return
|
||||
|
||||
await self.bot.initialize()
|
||||
@@ -154,12 +154,12 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
raise RuntimeError("This Updater is still running!")
|
||||
|
||||
if not self._initialized:
|
||||
self._logger.debug("This Updater is already shut down. Returning.")
|
||||
_LOGGER.debug("This Updater is already shut down. Returning.")
|
||||
return
|
||||
|
||||
await self.bot.shutdown()
|
||||
self._initialized = False
|
||||
self._logger.debug("Shut down of Updater complete")
|
||||
_LOGGER.debug("Shut down of Updater complete")
|
||||
|
||||
async def __aenter__(self: _UpdaterType) -> _UpdaterType:
|
||||
"""Simple context manager which initializes the Updater."""
|
||||
@@ -278,9 +278,9 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
error_callback=error_callback,
|
||||
)
|
||||
|
||||
self._logger.debug("Waiting for polling to start")
|
||||
_LOGGER.debug("Waiting for polling to start")
|
||||
await polling_ready.wait()
|
||||
self._logger.debug("Polling updates from Telegram started")
|
||||
_LOGGER.debug("Polling updates from Telegram started")
|
||||
|
||||
return self.update_queue
|
||||
except Exception as exc:
|
||||
@@ -301,7 +301,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
ready: asyncio.Event,
|
||||
error_callback: Optional[Callable[[TelegramError], None]],
|
||||
) -> None:
|
||||
self._logger.debug("Updater started (polling)")
|
||||
_LOGGER.debug("Updater started (polling)")
|
||||
|
||||
# the bootstrapping phase does two things:
|
||||
# 1) make sure there is no webhook set
|
||||
@@ -313,7 +313,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
allowed_updates=None,
|
||||
)
|
||||
|
||||
self._logger.debug("Bootstrap done")
|
||||
_LOGGER.debug("Bootstrap done")
|
||||
|
||||
async def polling_action_cb() -> bool:
|
||||
try:
|
||||
@@ -335,7 +335,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
raise exc
|
||||
except Exception as exc:
|
||||
# Other exceptions should not. Let's log them for now.
|
||||
self._logger.critical(
|
||||
_LOGGER.critical(
|
||||
"Something went wrong processing the data received from Telegram. "
|
||||
"Received data was *not* processed!",
|
||||
exc_info=exc,
|
||||
@@ -344,9 +344,9 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
|
||||
if updates:
|
||||
if not self.running:
|
||||
self._logger.critical(
|
||||
"Updater stopped unexpectedly. Pulled updates will be ignored and again "
|
||||
"on restart."
|
||||
_LOGGER.critical(
|
||||
"Updater stopped unexpectedly. Pulled updates will be ignored and pulled "
|
||||
"again on restart."
|
||||
)
|
||||
else:
|
||||
for update in updates:
|
||||
@@ -356,7 +356,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
return True # Keep fetching updates & don't quit. Polls with poll_interval.
|
||||
|
||||
def default_error_callback(exc: TelegramError) -> None:
|
||||
self._logger.exception("Exception happened while polling for updates.", exc_info=exc)
|
||||
_LOGGER.exception("Exception happened while polling for updates.", exc_info=exc)
|
||||
|
||||
# Start task that runs in background, pulls
|
||||
# updates from Telegram and inserts them in the update queue of the
|
||||
@@ -495,9 +495,9 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
secret_token=secret_token,
|
||||
)
|
||||
|
||||
self._logger.debug("Waiting for webhook server to start")
|
||||
_LOGGER.debug("Waiting for webhook server to start")
|
||||
await webhook_ready.wait()
|
||||
self._logger.debug("Webhook server started")
|
||||
_LOGGER.debug("Webhook server started")
|
||||
except Exception as exc:
|
||||
self._running = False
|
||||
raise exc
|
||||
@@ -521,7 +521,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
max_connections: int = 40,
|
||||
secret_token: str = None,
|
||||
) -> None:
|
||||
self._logger.debug("Updater thread started (webhook)")
|
||||
_LOGGER.debug("Updater thread started (webhook)")
|
||||
|
||||
if not url_path.startswith("/"):
|
||||
url_path = f"/{url_path}"
|
||||
@@ -599,7 +599,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
`action_cb`.
|
||||
|
||||
"""
|
||||
self._logger.debug("Start network loop retry %s", description)
|
||||
_LOGGER.debug("Start network loop retry %s", description)
|
||||
cur_interval = interval
|
||||
while self.running:
|
||||
try:
|
||||
@@ -607,17 +607,17 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
if not await action_cb():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
self._logger.info("%s", exc)
|
||||
_LOGGER.info("%s", exc)
|
||||
cur_interval = 0.5 + exc.retry_after
|
||||
except TimedOut as toe:
|
||||
self._logger.debug("Timed out %s: %s", description, toe)
|
||||
_LOGGER.debug("Timed out %s: %s", description, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken as pex:
|
||||
self._logger.error("Invalid token; aborting")
|
||||
_LOGGER.error("Invalid token; aborting")
|
||||
raise pex
|
||||
except TelegramError as telegram_exc:
|
||||
self._logger.error("Error while %s: %s", description, telegram_exc)
|
||||
_LOGGER.error("Error while %s: %s", description, telegram_exc)
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
# increase waiting times on subsequent errors up to 30secs
|
||||
@@ -629,7 +629,7 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
await asyncio.sleep(cur_interval)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
self._logger.debug("Network loop retry %s was cancelled", description)
|
||||
_LOGGER.debug("Network loop retry %s was cancelled", description)
|
||||
break
|
||||
|
||||
async def _bootstrap(
|
||||
@@ -651,16 +651,16 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
retries = 0
|
||||
|
||||
async def bootstrap_del_webhook() -> bool:
|
||||
self._logger.debug("Deleting webhook")
|
||||
_LOGGER.debug("Deleting webhook")
|
||||
if drop_pending_updates:
|
||||
self._logger.debug("Dropping pending updates from Telegram server")
|
||||
_LOGGER.debug("Dropping pending updates from Telegram server")
|
||||
await self.bot.delete_webhook(drop_pending_updates=drop_pending_updates)
|
||||
return False
|
||||
|
||||
async def bootstrap_set_webhook() -> bool:
|
||||
self._logger.debug("Setting webhook")
|
||||
_LOGGER.debug("Setting webhook")
|
||||
if drop_pending_updates:
|
||||
self._logger.debug("Dropping pending updates from Telegram server")
|
||||
_LOGGER.debug("Dropping pending updates from Telegram server")
|
||||
await self.bot.set_webhook(
|
||||
url=webhook_url,
|
||||
certificate=cert,
|
||||
@@ -679,11 +679,11 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
|
||||
if not isinstance(exc, InvalidToken) and (max_retries < 0 or retries < max_retries):
|
||||
retries += 1
|
||||
self._logger.warning(
|
||||
_LOGGER.warning(
|
||||
"Failed bootstrap phase; try=%s max_retries=%s", retries, max_retries
|
||||
)
|
||||
else:
|
||||
self._logger.error("Failed bootstrap phase after %s retries (%s)", retries, exc)
|
||||
_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
|
||||
@@ -724,33 +724,31 @@ class Updater(AsyncContextManager["Updater"]):
|
||||
if not self.running:
|
||||
raise RuntimeError("This Updater is not running!")
|
||||
|
||||
self._logger.debug("Stopping Updater")
|
||||
_LOGGER.debug("Stopping Updater")
|
||||
|
||||
self._running = False
|
||||
|
||||
await self._stop_httpd()
|
||||
await self._stop_polling()
|
||||
|
||||
self._logger.debug("Updater.stop() is complete")
|
||||
_LOGGER.debug("Updater.stop() is complete")
|
||||
|
||||
async def _stop_httpd(self) -> None:
|
||||
"""Stops the Webhook server by calling ``WebhookServer.shutdown()``"""
|
||||
if self._httpd:
|
||||
self._logger.debug("Waiting for current webhook connection to be closed.")
|
||||
_LOGGER.debug("Waiting for current webhook connection to be closed.")
|
||||
await self._httpd.shutdown()
|
||||
self._httpd = None
|
||||
|
||||
async def _stop_polling(self) -> None:
|
||||
"""Stops the polling task by awaiting it."""
|
||||
if self.__polling_task:
|
||||
self._logger.debug("Waiting background polling task to finish up.")
|
||||
_LOGGER.debug("Waiting background polling task to finish up.")
|
||||
self.__polling_task.cancel()
|
||||
|
||||
try:
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self.__polling_task
|
||||
except asyncio.CancelledError:
|
||||
# This only happens in rare edge-cases, e.g. when `stop()` is called directly
|
||||
# It only fails in rare edge-cases, e.g. when `stop()` is called directly
|
||||
# after start_polling(), but lets better be safe than sorry ...
|
||||
pass
|
||||
|
||||
self.__polling_task = None
|
||||
|
||||
@@ -25,12 +25,13 @@ Warning:
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from types import FrameType
|
||||
from typing import Optional
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from telegram._utils.logging import get_logger
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
def was_called_by(frame: Optional[FrameType], caller: Path) -> bool:
|
||||
@@ -57,7 +58,7 @@ def was_called_by(frame: Optional[FrameType], caller: Path) -> bool:
|
||||
try:
|
||||
return _was_called_by(frame, caller)
|
||||
except Exception as exc:
|
||||
_logger.debug(
|
||||
_LOGGER.debug(
|
||||
"Failed to check if frame was called by `caller`. Assuming that it was not.",
|
||||
exc_info=exc,
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# pylint: disable=missing-module-docstring
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from ssl import SSLContext
|
||||
from types import TracebackType
|
||||
@@ -31,11 +30,15 @@ import tornado.web
|
||||
from tornado.httpserver import HTTPServer
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram.ext._extbot import ExtBot
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
# This module is not visible to users, so we log as Updater
|
||||
_LOGGER = get_logger(__name__, class_name="Updater")
|
||||
|
||||
|
||||
class WebhookServer:
|
||||
"""Thin wrapper around ``tornado.httpserver.HTTPServer``."""
|
||||
@@ -44,7 +47,6 @@ class WebhookServer:
|
||||
"_http_server",
|
||||
"listen",
|
||||
"port",
|
||||
"_logger",
|
||||
"is_running",
|
||||
"_server_lock",
|
||||
"_shutdown_lock",
|
||||
@@ -56,7 +58,6 @@ class WebhookServer:
|
||||
self._http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx)
|
||||
self.listen = listen
|
||||
self.port = port
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self.is_running = False
|
||||
self._server_lock = asyncio.Lock()
|
||||
self._shutdown_lock = asyncio.Lock()
|
||||
@@ -69,17 +70,17 @@ class WebhookServer:
|
||||
if ready is not None:
|
||||
ready.set()
|
||||
|
||||
self._logger.debug("Webhook Server started.")
|
||||
_LOGGER.debug("Webhook Server started.")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
async with self._shutdown_lock:
|
||||
if not self.is_running:
|
||||
self._logger.debug("Webhook Server is already shut down. Returning")
|
||||
_LOGGER.debug("Webhook Server is already shut down. Returning")
|
||||
return
|
||||
self.is_running = False
|
||||
self._http_server.stop()
|
||||
await self._http_server.close_all_connections()
|
||||
self._logger.debug("Webhook Server stopped")
|
||||
_LOGGER.debug("Webhook Server stopped")
|
||||
|
||||
|
||||
class WebhookAppClass(tornado.web.Application):
|
||||
@@ -93,7 +94,7 @@ class WebhookAppClass(tornado.web.Application):
|
||||
"update_queue": update_queue,
|
||||
"secret_token": secret_token,
|
||||
}
|
||||
handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)] # noqa
|
||||
handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)]
|
||||
tornado.web.Application.__init__(self, handlers) # type: ignore
|
||||
|
||||
def log_request(self, handler: tornado.web.RequestHandler) -> None:
|
||||
@@ -104,7 +105,7 @@ class WebhookAppClass(tornado.web.Application):
|
||||
class TelegramHandler(tornado.web.RequestHandler):
|
||||
"""BaseHandler that processes incoming requests from Telegram"""
|
||||
|
||||
__slots__ = ("bot", "update_queue", "_logger", "secret_token")
|
||||
__slots__ = ("bot", "update_queue", "secret_token")
|
||||
|
||||
SUPPORTED_METHODS = ("POST",) # type: ignore[assignment]
|
||||
|
||||
@@ -113,10 +114,9 @@ class TelegramHandler(tornado.web.RequestHandler):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.bot = bot
|
||||
self.update_queue = update_queue # skipcq: PYL-W0201
|
||||
self._logger = logging.getLogger(__name__) # skipcq: PYL-W0201
|
||||
self.secret_token = secret_token # skipcq: PYL-W0201
|
||||
if secret_token:
|
||||
self._logger.debug(
|
||||
_LOGGER.debug(
|
||||
"The webhook server has a secret token, expecting it in incoming requests now"
|
||||
)
|
||||
|
||||
@@ -126,25 +126,25 @@ class TelegramHandler(tornado.web.RequestHandler):
|
||||
|
||||
async def post(self) -> None:
|
||||
"""Handle incoming POST request"""
|
||||
self._logger.debug("Webhook triggered")
|
||||
_LOGGER.debug("Webhook triggered")
|
||||
self._validate_post()
|
||||
|
||||
json_string = self.request.body.decode()
|
||||
data = json.loads(json_string)
|
||||
self.set_status(HTTPStatus.OK)
|
||||
self._logger.debug("Webhook received data: %s", json_string)
|
||||
_LOGGER.debug("Webhook received data: %s", json_string)
|
||||
|
||||
try:
|
||||
update = Update.de_json(data, self.bot)
|
||||
except Exception as exc:
|
||||
self._logger.critical(
|
||||
_LOGGER.critical(
|
||||
"Something went wrong processing the data received from Telegram. "
|
||||
"Received data was *not* processed!",
|
||||
exc_info=exc,
|
||||
)
|
||||
|
||||
if update:
|
||||
self._logger.debug(
|
||||
_LOGGER.debug(
|
||||
"Received Update with ID %d on Webhook",
|
||||
# For some reason pylint thinks update is a general TelegramObject
|
||||
update.update_id, # pylint: disable=no-member
|
||||
@@ -165,12 +165,12 @@ class TelegramHandler(tornado.web.RequestHandler):
|
||||
if self.secret_token is not None:
|
||||
token = self.request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
||||
if not token:
|
||||
self._logger.debug("Request did not include the secret token")
|
||||
_LOGGER.debug("Request did not include the secret token")
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.FORBIDDEN, reason="Request did not include the secret token"
|
||||
)
|
||||
if token != self.secret_token:
|
||||
self._logger.debug("Request had the wrong secret token: %s", token)
|
||||
_LOGGER.debug("Request had the wrong secret token: %s", token)
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.FORBIDDEN, reason="Request had the wrong secret token"
|
||||
)
|
||||
@@ -182,7 +182,7 @@ class TelegramHandler(tornado.web.RequestHandler):
|
||||
tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
"""Override the default logging and instead use our custom logging."""
|
||||
self._logger.debug(
|
||||
_LOGGER.debug(
|
||||
"%s - %s",
|
||||
self.request.remote_ip,
|
||||
"Exception in TelegramHandler",
|
||||
|
||||
@@ -141,7 +141,7 @@ class BaseFilter:
|
||||
|
||||
Also works with more than two filters::
|
||||
|
||||
filters.TEXT & (filters.Entity(URL) | filters.Entity(TEXT_LINK))
|
||||
filters.TEXT & (filters.Entity("url") | filters.Entity("text_link"))
|
||||
filters.TEXT & (~ filters.FORWARDED)
|
||||
|
||||
Note:
|
||||
@@ -244,7 +244,7 @@ class MessageFilter(BaseFilter):
|
||||
|
||||
Please see :class:`BaseFilter` for details on how to create custom filters.
|
||||
|
||||
.. seealso:: :wiki:`Advanced Filters <Extensions-–-Advanced-Filters>`
|
||||
.. seealso:: :wiki:`Advanced Filters <Extensions---Advanced-Filters>`
|
||||
|
||||
"""
|
||||
|
||||
@@ -379,7 +379,7 @@ class _MergedFilter(UpdateFilter):
|
||||
def _merge(base_output: Union[bool, Dict], comp_output: Union[bool, Dict]) -> FilterDataDict:
|
||||
base = base_output if isinstance(base_output, dict) else {}
|
||||
comp = comp_output if isinstance(comp_output, dict) else {}
|
||||
for k in comp.keys():
|
||||
for k in comp:
|
||||
# Make sure comp values are lists
|
||||
comp_value = comp[k] if isinstance(comp[k], list) else []
|
||||
try:
|
||||
@@ -387,7 +387,7 @@ class _MergedFilter(UpdateFilter):
|
||||
if isinstance(base[k], list):
|
||||
base[k] += comp_value
|
||||
else:
|
||||
base[k] = [base[k]] + comp_value
|
||||
base[k] = [base[k], *comp_value]
|
||||
except KeyError:
|
||||
base[k] = comp_value
|
||||
return base
|
||||
@@ -514,7 +514,7 @@ class Caption(MessageFilter):
|
||||
allow those whose caption is appearing in the given list.
|
||||
|
||||
Examples:
|
||||
``MessageHandler(filters.Caption(['PTB rocks!', 'PTB'], callback_method_2)``
|
||||
``MessageHandler(filters.Caption(['PTB rocks!', 'PTB']), callback_method_2)``
|
||||
|
||||
.. seealso::
|
||||
:attr:`telegram.ext.filters.CAPTION`
|
||||
|
||||
+15
-9
@@ -44,23 +44,27 @@ if TYPE_CHECKING:
|
||||
def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str:
|
||||
"""Helper function to escape telegram markup symbols.
|
||||
|
||||
.. versionchanged:: 20.3
|
||||
Custom emoji entity escaping is now supported.
|
||||
|
||||
Args:
|
||||
text (:obj:`str`): The text.
|
||||
version (:obj:`int` | :obj:`str`): Use to specify the version of telegrams Markdown.
|
||||
Either ``1`` or ``2``. Defaults to ``1``.
|
||||
entity_type (:obj:`str`, optional): For the entity types
|
||||
:tg-const:`telegram.MessageEntity.PRE`, :tg-const:`telegram.MessageEntity.CODE` and
|
||||
the link part of :tg-const:`telegram.MessageEntity.TEXT_LINK`, only certain characters
|
||||
need to be escaped in :tg-const:`telegram.constants.ParseMode.MARKDOWN_V2`.
|
||||
See the official API documentation for details. Only valid in combination with
|
||||
``version=2``, will be ignored else.
|
||||
the link part of :tg-const:`telegram.MessageEntity.TEXT_LINK` and
|
||||
:tg-const:`telegram.MessageEntity.CUSTOM_EMOJI`, only certain characters need to be
|
||||
escaped in :tg-const:`telegram.constants.ParseMode.MARKDOWN_V2`. See the `official API
|
||||
documentation <https://core.telegram.org/bots/api#formatting-options>`_ for details.
|
||||
Only valid in combination with ``version=2``, will be ignored else.
|
||||
"""
|
||||
if int(version) == 1:
|
||||
escape_chars = r"_*`["
|
||||
elif int(version) == 2:
|
||||
if entity_type in ["pre", "code"]:
|
||||
escape_chars = r"\`"
|
||||
elif entity_type == "text_link":
|
||||
elif entity_type in ["text_link", "custom_emoji"]:
|
||||
escape_chars = r"\)"
|
||||
else:
|
||||
escape_chars = r"\_*[]()~`>#+-=|{}.!"
|
||||
@@ -162,6 +166,11 @@ def create_deep_linked_url(bot_username: str, payload: str = None, group: bool =
|
||||
|
||||
Returns:
|
||||
:obj:`str`: An URL to start the bot with specific parameters.
|
||||
|
||||
Raises:
|
||||
:exc:`ValueError`: If the length of the :paramref:`payload` exceeds 64 characters,
|
||||
contains invalid characters, or if the :paramref:`bot_username` is less than 4
|
||||
characters.
|
||||
"""
|
||||
if bot_username is None or len(bot_username) <= 3:
|
||||
raise ValueError("You must provide a valid bot_username.")
|
||||
@@ -179,9 +188,6 @@ def create_deep_linked_url(bot_username: str, payload: str = None, group: bool =
|
||||
"URLs: A-Z, a-z, 0-9, _ and -"
|
||||
)
|
||||
|
||||
if group:
|
||||
key = "startgroup"
|
||||
else:
|
||||
key = "start"
|
||||
key = "startgroup" if group else "start"
|
||||
|
||||
return f"{base_url}?{key}={payload}"
|
||||
|
||||
@@ -26,6 +26,7 @@ from typing import AsyncContextManager, ClassVar, List, Optional, Tuple, Type, T
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE as _DEFAULT_NONE
|
||||
from telegram._utils.defaultvalue import DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
from telegram._version import __version__ as ptb_ver
|
||||
from telegram.error import (
|
||||
@@ -42,6 +43,8 @@ from telegram.request._requestdata import RequestData
|
||||
|
||||
RT = TypeVar("RT", bound="BaseRequest")
|
||||
|
||||
_LOGGER = get_logger(__name__, class_name="BaseRequest")
|
||||
|
||||
|
||||
class BaseRequest(
|
||||
AsyncContextManager["BaseRequest"],
|
||||
@@ -296,10 +299,7 @@ class BaseRequest(
|
||||
response_data = self.parse_json_payload(payload)
|
||||
|
||||
description = response_data.get("description")
|
||||
if description:
|
||||
message = description
|
||||
else:
|
||||
message = "Unknown HTTPError"
|
||||
message = description if description else "Unknown HTTPError"
|
||||
|
||||
# In some special cases, we can raise more informative exceptions:
|
||||
# see https://core.telegram.org/bots/api#responseparameters and
|
||||
@@ -354,6 +354,7 @@ class BaseRequest(
|
||||
try:
|
||||
return json.loads(decoded_s)
|
||||
except ValueError as exc:
|
||||
_LOGGER.error('Can not load invalid JSON data: "%s"', decoded_s)
|
||||
raise TelegramError("Invalid server response") from exc
|
||||
|
||||
@abc.abstractmethod
|
||||
|
||||
@@ -17,12 +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 methods to make POST and GET requests using the httpx library."""
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import httpx
|
||||
|
||||
from telegram._utils.defaultvalue import DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import ODVInput
|
||||
from telegram.error import NetworkError, TimedOut
|
||||
from telegram.request._baserequest import BaseRequest
|
||||
@@ -33,7 +33,7 @@ from telegram.request._requestdata import RequestData
|
||||
# https://www.python-httpx.org/contributing/#development-proxy-setup (also saved on archive.org)
|
||||
# That also works with socks5. Just pass `--mode socks5` to mitmproxy
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_LOGGER = get_logger(__name__, "HTTPXRequest")
|
||||
|
||||
|
||||
class HTTPXRequest(BaseRequest):
|
||||
@@ -122,7 +122,7 @@ class HTTPXRequest(BaseRequest):
|
||||
|
||||
# See https://github.com/python-telegram-bot/python-telegram-bot/pull/3542
|
||||
# for why we need to use `dict()` here.
|
||||
self._client_kwargs = dict( # pylint: disable=use-dict-literal
|
||||
self._client_kwargs = dict( # pylint: disable=use-dict-literal # noqa: C408
|
||||
timeout=timeout,
|
||||
proxies=proxy_url,
|
||||
limits=limits,
|
||||
@@ -166,7 +166,7 @@ class HTTPXRequest(BaseRequest):
|
||||
async def shutdown(self) -> None:
|
||||
"""See :meth:`BaseRequest.shutdown`."""
|
||||
if self._client.is_closed:
|
||||
_logger.debug("This HTTPXRequest is already shut down. Returning.")
|
||||
_LOGGER.debug("This HTTPXRequest is already shut down. Returning.")
|
||||
return
|
||||
|
||||
await self._client.aclose()
|
||||
|
||||
@@ -31,7 +31,7 @@ from telegram._utils.enum import StringEnum
|
||||
from telegram._utils.types import UploadFileDict
|
||||
|
||||
|
||||
@dataclass(repr=False, eq=False, order=False, frozen=True)
|
||||
@dataclass(repr=True, eq=False, order=False, frozen=True)
|
||||
class RequestParameter:
|
||||
"""Instances of this class represent a single parameter to be sent along with a request to
|
||||
the Bot API.
|
||||
|
||||
@@ -31,12 +31,15 @@ from tests.auxil.bot_method_checks import (
|
||||
check_shortcut_call,
|
||||
check_shortcut_signature,
|
||||
)
|
||||
from tests.auxil.deprecations import check_thumb_deprecation_warnings_for_args_and_attrs
|
||||
from tests.auxil.deprecations import (
|
||||
check_thumb_deprecation_warning_for_method_args,
|
||||
check_thumb_deprecation_warnings_for_args_and_attrs,
|
||||
)
|
||||
from tests.auxil.files import data_file
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def animation_file():
|
||||
with data_file("game.gif").open("rb") as f:
|
||||
yield f
|
||||
@@ -75,8 +78,8 @@ class TestAnimationWithoutRequest(TestAnimationBase):
|
||||
assert isinstance(animation, Animation)
|
||||
assert isinstance(animation.file_id, str)
|
||||
assert isinstance(animation.file_unique_id, str)
|
||||
assert animation.file_id != ""
|
||||
assert animation.file_unique_id != ""
|
||||
assert animation.file_id
|
||||
assert animation.file_unique_id
|
||||
|
||||
def test_expected_values(self, animation):
|
||||
assert animation.mime_type == self.mime_type
|
||||
@@ -191,6 +194,19 @@ class TestAnimationWithoutRequest(TestAnimationBase):
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
assert await bot.send_animation(animation=animation, chat_id=chat_id)
|
||||
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_send_animation_thumb_deprecation_warning(
|
||||
self, recwarn, monkeypatch, bot_class, bot, raw_bot, chat_id, animation
|
||||
):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
return True
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
await bot.send_animation(chat_id, animation, thumb="thumb")
|
||||
check_thumb_deprecation_warning_for_method_args(recwarn, __file__)
|
||||
|
||||
async def test_send_animation_with_local_files_throws_error_with_different_thumb_and_thumbnail(
|
||||
self, bot, chat_id
|
||||
):
|
||||
@@ -231,8 +247,8 @@ class TestAnimationWithRequest(TestAnimationBase):
|
||||
assert isinstance(message.animation, Animation)
|
||||
assert isinstance(message.animation.file_id, str)
|
||||
assert isinstance(message.animation.file_unique_id, str)
|
||||
assert message.animation.file_id != ""
|
||||
assert message.animation.file_unique_id != ""
|
||||
assert message.animation.file_id
|
||||
assert message.animation.file_unique_id
|
||||
assert message.animation.file_name == animation.file_name
|
||||
assert message.animation.mime_type == animation.mime_type
|
||||
assert message.animation.file_size == animation.file_size
|
||||
@@ -266,8 +282,8 @@ class TestAnimationWithRequest(TestAnimationBase):
|
||||
assert isinstance(message.animation, Animation)
|
||||
assert isinstance(message.animation.file_id, str)
|
||||
assert isinstance(message.animation.file_unique_id, str)
|
||||
assert message.animation.file_id != ""
|
||||
assert message.animation.file_unique_id != ""
|
||||
assert message.animation.file_id
|
||||
assert message.animation.file_unique_id
|
||||
|
||||
assert message.animation.duration == animation.duration
|
||||
assert message.animation.file_name.startswith(
|
||||
@@ -321,7 +337,7 @@ class TestAnimationWithRequest(TestAnimationBase):
|
||||
assert message.caption_markdown == escape_markdown(test_markdown_string)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -368,9 +384,7 @@ class TestAnimationWithRequest(TestAnimationBase):
|
||||
assert message.animation == animation
|
||||
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
animation_file = open(os.devnull, "rb")
|
||||
|
||||
with pytest.raises(TelegramError):
|
||||
with Path(os.devnull).open("rb") as animation_file, pytest.raises(TelegramError):
|
||||
await bot.send_animation(chat_id=chat_id, animation=animation_file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
|
||||
@@ -31,12 +31,15 @@ from tests.auxil.bot_method_checks import (
|
||||
check_shortcut_call,
|
||||
check_shortcut_signature,
|
||||
)
|
||||
from tests.auxil.deprecations import check_thumb_deprecation_warnings_for_args_and_attrs
|
||||
from tests.auxil.deprecations import (
|
||||
check_thumb_deprecation_warning_for_method_args,
|
||||
check_thumb_deprecation_warnings_for_args_and_attrs,
|
||||
)
|
||||
from tests.auxil.files import data_file
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def audio_file():
|
||||
with data_file("telegram.mp3").open("rb") as f:
|
||||
yield f
|
||||
@@ -77,8 +80,8 @@ class TestAudioWithoutRequest(TestAudioBase):
|
||||
assert isinstance(audio, Audio)
|
||||
assert isinstance(audio.file_id, str)
|
||||
assert isinstance(audio.file_unique_id, str)
|
||||
assert audio.file_id != ""
|
||||
assert audio.file_unique_id != ""
|
||||
assert audio.file_id
|
||||
assert audio.file_unique_id
|
||||
|
||||
def test_expected_values(self, audio):
|
||||
assert audio.duration == self.duration
|
||||
@@ -158,6 +161,19 @@ class TestAudioWithoutRequest(TestAudioBase):
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
assert await bot.send_audio(audio=audio, chat_id=chat_id)
|
||||
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_send_audio_thumb_deprecation_warning(
|
||||
self, recwarn, monkeypatch, bot_class, bot, raw_bot, chat_id, audio
|
||||
):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
return True
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
await bot.send_audio(chat_id, audio, thumb="thumb")
|
||||
check_thumb_deprecation_warning_for_method_args(recwarn, __file__)
|
||||
|
||||
async def test_send_audio_custom_filename(self, bot, chat_id, audio_file, monkeypatch):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
return list(request_data.multipart_data.values())[0][0] == "custom_filename"
|
||||
@@ -331,9 +347,7 @@ class TestAudioWithRequest(TestAudioBase):
|
||||
assert message.audio == audio
|
||||
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
audio_file = open(os.devnull, "rb")
|
||||
|
||||
with pytest.raises(TelegramError):
|
||||
with Path(os.devnull).open("rb") as audio_file, pytest.raises(TelegramError):
|
||||
await bot.send_audio(chat_id=chat_id, audio=audio_file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
|
||||
@@ -36,7 +36,7 @@ from tests.auxil.networking import expect_bad_request
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def chatphoto_file():
|
||||
with data_file("telegram.jpg").open("rb") as f:
|
||||
yield f
|
||||
@@ -174,7 +174,8 @@ class TestChatPhotoWithRequest:
|
||||
await file.download_to_drive(jpg_file)
|
||||
assert jpg_file.is_file()
|
||||
|
||||
assert "small" in asserts and "big" in asserts
|
||||
assert "small" in asserts
|
||||
assert "big" in asserts
|
||||
|
||||
async def test_send_all_args(self, bot, super_group_id, chatphoto_file):
|
||||
async def func():
|
||||
@@ -185,9 +186,7 @@ class TestChatPhotoWithRequest:
|
||||
)
|
||||
|
||||
async def test_error_send_empty_file(self, bot, super_group_id):
|
||||
chatphoto_file = open(os.devnull, "rb")
|
||||
|
||||
with pytest.raises(TelegramError):
|
||||
with Path(os.devnull).open("rb") as chatphoto_file, pytest.raises(TelegramError):
|
||||
await bot.set_chat_photo(chat_id=super_group_id, photo=chatphoto_file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, super_group_id):
|
||||
|
||||
@@ -129,7 +129,7 @@ class TestContactWithoutRequest(TestContactBase):
|
||||
|
||||
class TestContactWithRequest(TestContactBase):
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
|
||||
@@ -31,12 +31,15 @@ from tests.auxil.bot_method_checks import (
|
||||
check_shortcut_call,
|
||||
check_shortcut_signature,
|
||||
)
|
||||
from tests.auxil.deprecations import check_thumb_deprecation_warnings_for_args_and_attrs
|
||||
from tests.auxil.deprecations import (
|
||||
check_thumb_deprecation_warning_for_method_args,
|
||||
check_thumb_deprecation_warnings_for_args_and_attrs,
|
||||
)
|
||||
from tests.auxil.files import data_file
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def document_file():
|
||||
with data_file("telegram.png").open("rb") as f:
|
||||
yield f
|
||||
@@ -71,8 +74,8 @@ class TestDocumentWithoutRequest(TestDocumentBase):
|
||||
assert isinstance(document, Document)
|
||||
assert isinstance(document.file_id, str)
|
||||
assert isinstance(document.file_unique_id, str)
|
||||
assert document.file_id != ""
|
||||
assert document.file_unique_id != ""
|
||||
assert document.file_id
|
||||
assert document.file_unique_id
|
||||
|
||||
def test_expected_values(self, document):
|
||||
assert document.file_size == self.file_size
|
||||
@@ -157,6 +160,19 @@ class TestDocumentWithoutRequest(TestDocumentBase):
|
||||
|
||||
assert message
|
||||
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_send_document_thumb_deprecation_warning(
|
||||
self, recwarn, monkeypatch, bot_class, bot, raw_bot, chat_id, document
|
||||
):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
return True
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
await bot.send_document(chat_id, document, thumb="thumb")
|
||||
check_thumb_deprecation_warning_for_method_args(recwarn, __file__)
|
||||
|
||||
@pytest.mark.parametrize("local_mode", [True, False])
|
||||
async def test_send_document_local_files(self, monkeypatch, bot, chat_id, local_mode):
|
||||
try:
|
||||
@@ -206,9 +222,8 @@ class TestDocumentWithoutRequest(TestDocumentBase):
|
||||
|
||||
class TestDocumentWithRequest(TestDocumentBase):
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
with open(os.devnull, "rb") as f:
|
||||
with pytest.raises(TelegramError):
|
||||
await bot.send_document(chat_id=chat_id, document=f)
|
||||
with Path(os.devnull).open("rb") as f, pytest.raises(TelegramError):
|
||||
await bot.send_document(chat_id=chat_id, document=f)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
@@ -247,9 +262,9 @@ class TestDocumentWithRequest(TestDocumentBase):
|
||||
|
||||
assert isinstance(message.document, Document)
|
||||
assert isinstance(message.document.file_id, str)
|
||||
assert message.document.file_id != ""
|
||||
assert message.document.file_id
|
||||
assert isinstance(message.document.file_unique_id, str)
|
||||
assert message.document.file_unique_id != ""
|
||||
assert message.document.file_unique_id
|
||||
assert isinstance(message.document.thumbnail, PhotoSize)
|
||||
assert message.document.file_name == "telegram_custom.png"
|
||||
assert message.document.mime_type == document.mime_type
|
||||
@@ -266,9 +281,9 @@ class TestDocumentWithRequest(TestDocumentBase):
|
||||
|
||||
assert isinstance(document, Document)
|
||||
assert isinstance(document.file_id, str)
|
||||
assert document.file_id != ""
|
||||
assert document.file_id
|
||||
assert isinstance(message.document.file_unique_id, str)
|
||||
assert message.document.file_unique_id != ""
|
||||
assert message.document.file_unique_id
|
||||
assert isinstance(document.thumbnail, PhotoSize)
|
||||
assert document.file_name == "telegram.gif"
|
||||
assert document.mime_type == "image/gif"
|
||||
@@ -328,7 +343,7 @@ class TestDocumentWithRequest(TestDocumentBase):
|
||||
assert message.caption_markdown == escape_markdown(test_markdown_string)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import contextlib
|
||||
import subprocess
|
||||
import sys
|
||||
from io import BytesIO
|
||||
@@ -49,12 +50,10 @@ class TestInputFileWithoutRequest:
|
||||
assert in_file.mimetype == "application/octet-stream"
|
||||
assert in_file.filename == "application.octet-stream"
|
||||
|
||||
try:
|
||||
with contextlib.suppress(ProcessLookupError):
|
||||
proc.kill()
|
||||
except ProcessLookupError:
|
||||
# This exception may be thrown if the process has finished before we had the chance
|
||||
# to kill it.
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("attach", [True, False])
|
||||
def test_attach(self, attach):
|
||||
|
||||
@@ -54,7 +54,7 @@ from .test_audio import audio, audio_file # noqa: F401
|
||||
from .test_document import document, document_file # noqa: F401
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from .test_photo import _photo, photo, photo_file, thumb # noqa: F401
|
||||
from .test_photo import photo, photo_file, photolist, thumb # noqa: F401
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from .test_video import video, video_file # noqa: F401
|
||||
@@ -211,7 +211,7 @@ class TestInputMediaVideoWithoutRequest(TestInputMediaVideoBase):
|
||||
assert input_media_video.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
|
||||
def test_with_local_files_throws_exception_with_different_thumb_and_thumbnail(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="You passed different entities as 'thumb' and "):
|
||||
InputMediaVideo(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
@@ -351,7 +351,7 @@ class TestInputMediaAnimationWithoutRequest(TestInputMediaAnimationBase):
|
||||
assert input_media_animation.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
|
||||
def test_with_local_files_throws_exception_with_different_thumb_and_thumbnail(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="You passed different entities as 'thumb' and "):
|
||||
InputMediaAnimation(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
@@ -436,7 +436,7 @@ class TestInputMediaAudioWithoutRequest(TestInputMediaAudioBase):
|
||||
assert input_media_audio.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
|
||||
def test_with_local_files_throws_exception_with_different_thumb_and_thumbnail(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="You passed different entities as 'thumb' and "):
|
||||
InputMediaAudio(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
@@ -518,7 +518,7 @@ class TestInputMediaDocumentWithoutRequest(TestInputMediaDocumentBase):
|
||||
assert input_media_document.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
|
||||
def test_with_local_files_throws_exception_with_different_thumb_and_thumbnail(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="You passed different entities as 'thumb' and "):
|
||||
InputMediaDocument(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
@@ -526,7 +526,7 @@ class TestInputMediaDocumentWithoutRequest(TestInputMediaDocumentBase):
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module") # noqa: F811
|
||||
@pytest.fixture(scope="module")
|
||||
def media_group(photo, thumb): # noqa: F811
|
||||
return [
|
||||
InputMediaPhoto(photo, caption="*photo* 1", parse_mode="Markdown"),
|
||||
@@ -537,12 +537,12 @@ def media_group(photo, thumb): # noqa: F811
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module") # noqa: F811
|
||||
@pytest.fixture(scope="module")
|
||||
def media_group_no_caption_args(photo, thumb): # noqa: F811
|
||||
return [InputMediaPhoto(photo), InputMediaPhoto(thumb), InputMediaPhoto(photo)]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module") # noqa: F811
|
||||
@pytest.fixture(scope="module")
|
||||
def media_group_no_caption_only_caption_entities(photo, thumb): # noqa: F811
|
||||
return [
|
||||
InputMediaPhoto(photo, caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 5)]),
|
||||
@@ -550,7 +550,7 @@ def media_group_no_caption_only_caption_entities(photo, thumb): # noqa: F811
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module") # noqa: F811
|
||||
@pytest.fixture(scope="module")
|
||||
def media_group_no_caption_only_parse_mode(photo, thumb): # noqa: F811
|
||||
return [
|
||||
InputMediaPhoto(photo, parse_mode="Markdown"),
|
||||
@@ -720,7 +720,7 @@ class TestSendMediaGroupWithRequest:
|
||||
assert all(i.message_thread_id == real_topic.message_thread_id for i in messages)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"caption, parse_mode, caption_entities",
|
||||
("caption", "parse_mode", "caption_entities"),
|
||||
[
|
||||
# same combinations of caption options as in media_group fixture
|
||||
("*photo* 1", "Markdown", None),
|
||||
@@ -852,7 +852,7 @@ class TestSendMediaGroupWithRequest:
|
||||
assert isinstance(new_message, Message)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -984,6 +984,7 @@ class TestSendMediaGroupWithRequest:
|
||||
return InputMediaPhoto(photo, **kwargs)
|
||||
if med_type == "video":
|
||||
return InputMediaVideo(video, **kwargs)
|
||||
return None
|
||||
|
||||
message = await default_bot.send_photo(chat_id, photo)
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ class TestInputStickerNoRequest(TestInputStickerBase):
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_expected_values(self, input_sticker):
|
||||
assert input_sticker.sticker == self.sticker and isinstance(input_sticker.sticker, str)
|
||||
assert input_sticker.sticker == self.sticker
|
||||
assert isinstance(input_sticker.sticker, str)
|
||||
assert input_sticker.emoji_list == self.emoji_list
|
||||
assert input_sticker.mask_position == self.mask_position
|
||||
assert input_sticker.keywords == self.keywords
|
||||
@@ -60,7 +61,8 @@ class TestInputStickerNoRequest(TestInputStickerBase):
|
||||
assert isinstance(input_sticker.keywords, tuple)
|
||||
assert isinstance(input_sticker.emoji_list, tuple)
|
||||
a = InputSticker("sticker", ["emoji"])
|
||||
assert isinstance(a.emoji_list, tuple) and a.keywords == ()
|
||||
assert isinstance(a.emoji_list, tuple)
|
||||
assert a.keywords == ()
|
||||
|
||||
def test_to_dict(self, input_sticker):
|
||||
input_sticker_dict = input_sticker.to_dict()
|
||||
|
||||
@@ -136,8 +136,7 @@ class TestLocationWithoutRequest(TestLocationBase):
|
||||
# TODO: Needs improvement with in inline sent live location.
|
||||
async def test_stop_live_inline_message(self, monkeypatch, bot):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
id_ = request_data.json_parameters["inline_message_id"] == "1234"
|
||||
return id_
|
||||
return request_data.json_parameters["inline_message_id"] == "1234"
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
assert await bot.stop_message_live_location(inline_message_id=1234)
|
||||
@@ -163,7 +162,7 @@ class TestLocationWithoutRequest(TestLocationBase):
|
||||
|
||||
class TestLocationWithRequest:
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -205,7 +204,7 @@ class TestLocationWithRequest:
|
||||
assert protected.has_protected_content
|
||||
assert not unprotected.has_protected_content
|
||||
|
||||
@pytest.mark.xfail
|
||||
@pytest.mark.xfail()
|
||||
async def test_send_live_location(self, bot, chat_id):
|
||||
message = await bot.send_location(
|
||||
chat_id=chat_id,
|
||||
@@ -218,8 +217,8 @@ class TestLocationWithRequest:
|
||||
protect_content=True,
|
||||
)
|
||||
assert message.location
|
||||
assert 52.223880 == pytest.approx(message.location.latitude, rel=1e-5)
|
||||
assert 5.166146 == pytest.approx(message.location.longitude, rel=1e-5)
|
||||
assert pytest.approx(message.location.latitude, rel=1e-5) == 52.223880
|
||||
assert pytest.approx(message.location.longitude, rel=1e-5) == 5.166146
|
||||
assert message.location.live_period == 80
|
||||
assert message.location.horizontal_accuracy == 50
|
||||
assert message.location.heading == 90
|
||||
@@ -236,8 +235,8 @@ class TestLocationWithRequest:
|
||||
proximity_alert_radius=500,
|
||||
)
|
||||
|
||||
assert 52.223098 == pytest.approx(message2.location.latitude, rel=1e-5)
|
||||
assert 5.164306 == pytest.approx(message2.location.longitude, rel=1e-5)
|
||||
assert pytest.approx(message2.location.latitude, rel=1e-5) == 52.223098
|
||||
assert pytest.approx(message2.location.longitude, rel=1e-5) == 5.164306
|
||||
assert message2.location.horizontal_accuracy == 30
|
||||
assert message2.location.heading == 10
|
||||
assert message2.location.proximity_alert_radius == 500
|
||||
|
||||
+39
-39
@@ -36,14 +36,14 @@ from tests.auxil.networking import expect_bad_request
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def photo_file():
|
||||
with data_file("telegram.jpg").open("rb") as f:
|
||||
yield f
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
async def _photo(bot, chat_id):
|
||||
async def photolist(bot, chat_id):
|
||||
async def func():
|
||||
with data_file("telegram.jpg").open("rb") as f:
|
||||
return (await bot.send_photo(chat_id, photo=f, read_timeout=50)).photo
|
||||
@@ -54,13 +54,13 @@ async def _photo(bot, chat_id):
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def thumb(_photo):
|
||||
return _photo[0]
|
||||
def thumb(photolist):
|
||||
return photolist[0]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photo(_photo):
|
||||
return _photo[-1]
|
||||
def photo(photolist):
|
||||
return photolist[-1]
|
||||
|
||||
|
||||
class TestPhotoBase:
|
||||
@@ -84,14 +84,14 @@ class TestPhotoWithoutRequest(TestPhotoBase):
|
||||
assert isinstance(photo, PhotoSize)
|
||||
assert isinstance(photo.file_id, str)
|
||||
assert isinstance(photo.file_unique_id, str)
|
||||
assert photo.file_id != ""
|
||||
assert photo.file_unique_id != ""
|
||||
assert photo.file_id
|
||||
assert photo.file_unique_id
|
||||
|
||||
assert isinstance(thumb, PhotoSize)
|
||||
assert isinstance(thumb.file_id, str)
|
||||
assert isinstance(thumb.file_unique_id, str)
|
||||
assert thumb.file_id != ""
|
||||
assert thumb.file_unique_id != ""
|
||||
assert thumb.file_id
|
||||
assert thumb.file_unique_id
|
||||
|
||||
def test_expected_values(self, photo, thumb):
|
||||
assert photo.width == self.width
|
||||
@@ -223,14 +223,14 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert isinstance(message.photo[-2], PhotoSize)
|
||||
assert isinstance(message.photo[-2].file_id, str)
|
||||
assert isinstance(message.photo[-2].file_unique_id, str)
|
||||
assert message.photo[-2].file_id != ""
|
||||
assert message.photo[-2].file_unique_id != ""
|
||||
assert message.photo[-2].file_id
|
||||
assert message.photo[-2].file_unique_id
|
||||
|
||||
assert isinstance(message.photo[-1], PhotoSize)
|
||||
assert isinstance(message.photo[-1].file_id, str)
|
||||
assert isinstance(message.photo[-1].file_unique_id, str)
|
||||
assert message.photo[-1].file_id != ""
|
||||
assert message.photo[-1].file_unique_id != ""
|
||||
assert message.photo[-1].file_id
|
||||
assert message.photo[-1].file_unique_id
|
||||
|
||||
assert message.caption == self.caption.replace("*", "")
|
||||
assert message.has_protected_content
|
||||
@@ -243,14 +243,14 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert isinstance(message.photo[-2], PhotoSize)
|
||||
assert isinstance(message.photo[-2].file_id, str)
|
||||
assert isinstance(message.photo[-2].file_unique_id, str)
|
||||
assert message.photo[-2].file_id != ""
|
||||
assert message.photo[-2].file_unique_id != ""
|
||||
assert message.photo[-2].file_id
|
||||
assert message.photo[-2].file_unique_id
|
||||
|
||||
assert isinstance(message.photo[-1], PhotoSize)
|
||||
assert isinstance(message.photo[-1].file_id, str)
|
||||
assert isinstance(message.photo[-1].file_unique_id, str)
|
||||
assert message.photo[-1].file_id != ""
|
||||
assert message.photo[-1].file_unique_id != ""
|
||||
assert message.photo[-1].file_id
|
||||
assert message.photo[-1].file_unique_id
|
||||
|
||||
assert message.caption == self.caption.replace("*", "")
|
||||
assert len(message.caption_entities) == 1
|
||||
@@ -262,14 +262,14 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert isinstance(message.photo[-2], PhotoSize)
|
||||
assert isinstance(message.photo[-2].file_id, str)
|
||||
assert isinstance(message.photo[-2].file_unique_id, str)
|
||||
assert message.photo[-2].file_id != ""
|
||||
assert message.photo[-2].file_unique_id != ""
|
||||
assert message.photo[-2].file_id
|
||||
assert message.photo[-2].file_unique_id
|
||||
|
||||
assert isinstance(message.photo[-1], PhotoSize)
|
||||
assert isinstance(message.photo[-1].file_id, str)
|
||||
assert isinstance(message.photo[-1].file_unique_id, str)
|
||||
assert message.photo[-1].file_id != ""
|
||||
assert message.photo[-1].file_unique_id != ""
|
||||
assert message.photo[-1].file_id
|
||||
assert message.photo[-1].file_unique_id
|
||||
|
||||
assert message.caption == self.caption.replace("<b>", "").replace("</b>", "")
|
||||
assert len(message.caption_entities) == 1
|
||||
@@ -328,7 +328,7 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert not unprotected.has_protected_content
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -381,14 +381,14 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert isinstance(message.photo[-2], PhotoSize)
|
||||
assert isinstance(message.photo[-2].file_id, str)
|
||||
assert isinstance(message.photo[-2].file_unique_id, str)
|
||||
assert message.photo[-2].file_id != ""
|
||||
assert message.photo[-2].file_unique_id != ""
|
||||
assert message.photo[-2].file_id
|
||||
assert message.photo[-2].file_unique_id
|
||||
|
||||
assert isinstance(message.photo[-1], PhotoSize)
|
||||
assert isinstance(message.photo[-1].file_id, str)
|
||||
assert isinstance(message.photo[-1].file_unique_id, str)
|
||||
assert message.photo[-1].file_id != ""
|
||||
assert message.photo[-1].file_unique_id != ""
|
||||
assert message.photo[-1].file_id
|
||||
assert message.photo[-1].file_unique_id
|
||||
|
||||
async def test_send_url_png_file(self, bot, chat_id):
|
||||
message = await bot.send_photo(
|
||||
@@ -400,8 +400,8 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert isinstance(photo, PhotoSize)
|
||||
assert isinstance(photo.file_id, str)
|
||||
assert isinstance(photo.file_unique_id, str)
|
||||
assert photo.file_id != ""
|
||||
assert photo.file_unique_id != ""
|
||||
assert photo.file_id
|
||||
assert photo.file_unique_id
|
||||
|
||||
async def test_send_file_unicode_filename(self, bot, chat_id):
|
||||
"""
|
||||
@@ -415,8 +415,8 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert isinstance(photo, PhotoSize)
|
||||
assert isinstance(photo.file_id, str)
|
||||
assert isinstance(photo.file_unique_id, str)
|
||||
assert photo.file_id != ""
|
||||
assert photo.file_unique_id != ""
|
||||
assert photo.file_id
|
||||
assert photo.file_unique_id
|
||||
|
||||
async def test_send_bytesio_jpg_file(self, bot, chat_id):
|
||||
filepath = data_file("telegram_no_standard_header.jpg")
|
||||
@@ -438,8 +438,8 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
photo = message.photo[-1]
|
||||
assert isinstance(photo.file_id, str)
|
||||
assert isinstance(photo.file_unique_id, str)
|
||||
assert photo.file_id != ""
|
||||
assert photo.file_unique_id != ""
|
||||
assert photo.file_id
|
||||
assert photo.file_unique_id
|
||||
assert isinstance(photo, PhotoSize)
|
||||
assert photo.width == 1280
|
||||
assert photo.height == 720
|
||||
@@ -451,18 +451,18 @@ class TestPhotoWithRequest(TestPhotoBase):
|
||||
assert isinstance(message.photo[-2], PhotoSize)
|
||||
assert isinstance(message.photo[-2].file_id, str)
|
||||
assert isinstance(message.photo[-2].file_unique_id, str)
|
||||
assert message.photo[-2].file_id != ""
|
||||
assert message.photo[-2].file_unique_id != ""
|
||||
assert message.photo[-2].file_id
|
||||
assert message.photo[-2].file_unique_id
|
||||
|
||||
assert isinstance(message.photo[-1], PhotoSize)
|
||||
assert isinstance(message.photo[-1].file_id, str)
|
||||
assert isinstance(message.photo[-1].file_unique_id, str)
|
||||
assert message.photo[-1].file_id != ""
|
||||
assert message.photo[-1].file_unique_id != ""
|
||||
assert message.photo[-1].file_id
|
||||
assert message.photo[-1].file_unique_id
|
||||
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
await bot.send_photo(chat_id=chat_id, photo=open(os.devnull, "rb"))
|
||||
with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError):
|
||||
await bot.send_photo(chat_id=chat_id, photo=file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
|
||||
+103
-65
@@ -18,6 +18,8 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -35,7 +37,6 @@ from telegram import (
|
||||
)
|
||||
from telegram.constants import StickerFormat
|
||||
from telegram.error import BadRequest, TelegramError
|
||||
from telegram.ext import ExtBot
|
||||
from telegram.request import RequestData
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.bot_method_checks import (
|
||||
@@ -48,7 +49,7 @@ from tests.auxil.files import data_file
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def sticker_file():
|
||||
with data_file("telegram.webp").open("rb") as file:
|
||||
yield file
|
||||
@@ -64,7 +65,7 @@ async def sticker(bot, chat_id):
|
||||
return sticker
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def animated_sticker_file():
|
||||
with data_file("telegram_animated_sticker.tgs").open("rb") as f:
|
||||
yield f
|
||||
@@ -76,7 +77,7 @@ async def animated_sticker(bot, chat_id):
|
||||
return (await bot.send_sticker(chat_id, sticker=f, read_timeout=50)).sticker
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def video_sticker_file():
|
||||
with data_file("telegram_video_sticker.webm").open("rb") as f:
|
||||
yield f
|
||||
@@ -126,13 +127,13 @@ class TestStickerWithoutRequest(TestStickerBase):
|
||||
assert isinstance(sticker, Sticker)
|
||||
assert isinstance(sticker.file_id, str)
|
||||
assert isinstance(sticker.file_unique_id, str)
|
||||
assert sticker.file_id != ""
|
||||
assert sticker.file_unique_id != ""
|
||||
assert sticker.file_id
|
||||
assert sticker.file_unique_id
|
||||
assert isinstance(sticker.thumbnail, PhotoSize)
|
||||
assert isinstance(sticker.thumbnail.file_id, str)
|
||||
assert isinstance(sticker.thumbnail.file_unique_id, str)
|
||||
assert sticker.thumbnail.file_id != ""
|
||||
assert sticker.thumbnail.file_unique_id != ""
|
||||
assert sticker.thumbnail.file_id
|
||||
assert sticker.thumbnail.file_unique_id
|
||||
assert isinstance(sticker.needs_repainting, bool)
|
||||
|
||||
def test_expected_values(self, sticker):
|
||||
@@ -312,8 +313,8 @@ class TestStickerWithRequest(TestStickerBase):
|
||||
assert isinstance(message.sticker, Sticker)
|
||||
assert isinstance(message.sticker.file_id, str)
|
||||
assert isinstance(message.sticker.file_unique_id, str)
|
||||
assert message.sticker.file_id != ""
|
||||
assert message.sticker.file_unique_id != ""
|
||||
assert message.sticker.file_id
|
||||
assert message.sticker.file_unique_id
|
||||
assert message.sticker.width == sticker.width
|
||||
assert message.sticker.height == sticker.height
|
||||
assert message.sticker.is_animated == sticker.is_animated
|
||||
@@ -327,8 +328,8 @@ class TestStickerWithRequest(TestStickerBase):
|
||||
assert isinstance(message.sticker.thumbnail, PhotoSize)
|
||||
assert isinstance(message.sticker.thumbnail.file_id, str)
|
||||
assert isinstance(message.sticker.thumbnail.file_unique_id, str)
|
||||
assert message.sticker.thumbnail.file_id != ""
|
||||
assert message.sticker.thumbnail.file_unique_id != ""
|
||||
assert message.sticker.thumbnail.file_id
|
||||
assert message.sticker.thumbnail.file_unique_id
|
||||
assert message.sticker.thumbnail.width == sticker.thumbnail.width
|
||||
assert message.sticker.thumbnail.height == sticker.thumbnail.height
|
||||
assert message.sticker.thumbnail.file_size == sticker.thumbnail.file_size
|
||||
@@ -372,8 +373,8 @@ class TestStickerWithRequest(TestStickerBase):
|
||||
assert isinstance(message.sticker, Sticker)
|
||||
assert isinstance(message.sticker.file_id, str)
|
||||
assert isinstance(message.sticker.file_unique_id, str)
|
||||
assert message.sticker.file_id != ""
|
||||
assert message.sticker.file_unique_id != ""
|
||||
assert message.sticker.file_id
|
||||
assert message.sticker.file_unique_id
|
||||
assert message.sticker.width == sticker.width
|
||||
assert message.sticker.height == sticker.height
|
||||
assert message.sticker.is_animated == sticker.is_animated
|
||||
@@ -384,14 +385,14 @@ class TestStickerWithRequest(TestStickerBase):
|
||||
assert isinstance(message.sticker.thumbnail, PhotoSize)
|
||||
assert isinstance(message.sticker.thumbnail.file_id, str)
|
||||
assert isinstance(message.sticker.thumbnail.file_unique_id, str)
|
||||
assert message.sticker.thumbnail.file_id != ""
|
||||
assert message.sticker.thumbnail.file_unique_id != ""
|
||||
assert message.sticker.thumbnail.file_id
|
||||
assert message.sticker.thumbnail.file_unique_id
|
||||
assert message.sticker.thumbnail.width == sticker.thumbnail.width
|
||||
assert message.sticker.thumbnail.height == sticker.thumbnail.height
|
||||
assert message.sticker.thumbnail.file_size == sticker.thumbnail.file_size
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -442,7 +443,7 @@ class TestStickerWithRequest(TestStickerBase):
|
||||
premium_sticker = premium_sticker_set.stickers[20]
|
||||
assert premium_sticker.premium_animation.file_unique_id == "AQADOBwAAifPOElr"
|
||||
assert isinstance(premium_sticker.premium_animation.file_id, str)
|
||||
assert premium_sticker.premium_animation.file_id != ""
|
||||
assert premium_sticker.premium_animation.file_id
|
||||
premium_sticker_dict = {
|
||||
"file_unique_id": "AQADOBwAAifPOElr",
|
||||
"file_id": premium_sticker.premium_animation.file_id,
|
||||
@@ -478,15 +479,15 @@ class TestStickerWithRequest(TestStickerBase):
|
||||
assert emoji_sticker_list[0].file_unique_id == "AgAD6gwAAoY06FM"
|
||||
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
await bot.send_sticker(chat_id, open(os.devnull, "rb"))
|
||||
with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError):
|
||||
await bot.send_sticker(chat_id, file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
await bot.send_sticker(chat_id, "")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
async def sticker_set(bot):
|
||||
ss = await bot.get_sticker_set(f"test_by_{bot.username}")
|
||||
if len(ss.stickers) > 100:
|
||||
@@ -496,11 +497,11 @@ async def sticker_set(bot):
|
||||
except BadRequest as e:
|
||||
if e.message == "Stickerset_not_modified":
|
||||
return ss
|
||||
raise Exception("stickerset is growing too large.")
|
||||
raise Exception("stickerset is growing too large.") from None
|
||||
return ss
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
async def animated_sticker_set(bot):
|
||||
ss = await bot.get_sticker_set(f"animated_test_by_{bot.username}")
|
||||
if len(ss.stickers) > 100:
|
||||
@@ -510,11 +511,11 @@ async def animated_sticker_set(bot):
|
||||
except BadRequest as e:
|
||||
if e.message == "Stickerset_not_modified":
|
||||
return ss
|
||||
raise Exception("stickerset is growing too large.")
|
||||
raise Exception("stickerset is growing too large.") from None
|
||||
return ss
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
async def video_sticker_set(bot):
|
||||
ss = await bot.get_sticker_set(f"video_test_by_{bot.username}")
|
||||
if len(ss.stickers) > 100:
|
||||
@@ -524,11 +525,11 @@ async def video_sticker_set(bot):
|
||||
except BadRequest as e:
|
||||
if e.message == "Stickerset_not_modified":
|
||||
return ss
|
||||
raise Exception("stickerset is growing too large.")
|
||||
raise Exception("stickerset is growing too large.") from None
|
||||
return ss
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def sticker_set_thumb_file():
|
||||
with data_file("sticker_set_thumb.png").open("rb") as file:
|
||||
yield file
|
||||
@@ -640,15 +641,20 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
async def test_upload_sticker_file_warning(self, bot, monkeypatch, chat_id, recwarn):
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_upload_sticker_file_warning(
|
||||
self, bot, raw_bot, monkeypatch, chat_id, recwarn, bot_class
|
||||
):
|
||||
async def make_assertion(*args, **kwargs):
|
||||
return {"file_id": "file_id", "file_unique_id": "file_unique_id"}
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
monkeypatch.setattr(bot, "_post", make_assertion)
|
||||
|
||||
await bot.upload_sticker_file(chat_id, "png_sticker_file_id")
|
||||
assert len(recwarn) == 1
|
||||
assert "Since Bot API 6.6, the parameter" in str(recwarn[0].message)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__
|
||||
|
||||
async def test_upload_sticker_file_missing_required_args(self, bot, chat_id):
|
||||
@@ -692,19 +698,31 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
|
||||
test_flag = False
|
||||
await bot.upload_sticker_file(chat_id, file)
|
||||
assert test_flag
|
||||
assert len(recwarn) in (1, 2) # second warning is for unclosed file
|
||||
|
||||
warnings = [w for w in recwarn if w.category is not ResourceWarning]
|
||||
assert len(warnings) == 1
|
||||
assert warnings[0].category is PTBDeprecationWarning
|
||||
assert warnings[0].filename == __file__
|
||||
assert str(warnings[0].message).startswith(
|
||||
"Since Bot API 6.6, the parameter `png_sticker` for "
|
||||
)
|
||||
finally:
|
||||
bot._local_mode = False
|
||||
|
||||
async def test_create_new_sticker_set_warning(self, bot, monkeypatch, chat_id, recwarn):
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_create_new_sticker_set_warning(
|
||||
self, bot, raw_bot, bot_class, monkeypatch, chat_id, recwarn
|
||||
):
|
||||
async def make_assertion(*args, **kwargs):
|
||||
return True
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
monkeypatch.setattr(bot, "_post", make_assertion)
|
||||
|
||||
await bot.create_new_sticker_set(chat_id, "name", "title", "some_str_emoji")
|
||||
assert len(recwarn) == 1
|
||||
assert "Since Bot API 6.6, the parameters" in str(recwarn[0].message)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__
|
||||
|
||||
async def test_create_new_sticker_set_missing_required_args(self, bot, chat_id):
|
||||
@@ -767,7 +785,13 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
|
||||
sticker_format=StickerFormat.STATIC,
|
||||
)
|
||||
assert test_flag
|
||||
assert len(recwarn) in (1, 2)
|
||||
|
||||
warnings = [w for w in recwarn if w.category is not ResourceWarning]
|
||||
assert len(warnings) == 1
|
||||
assert warnings[0].category is PTBDeprecationWarning
|
||||
assert warnings[0].filename == __file__
|
||||
assert str(warnings[0].message).startswith("Since Bot API 6.6, the parameters")
|
||||
assert "for `create_new_sticker_set` are deprecated" in str(warnings[0].message)
|
||||
|
||||
async def test_create_new_sticker_all_params(
|
||||
self, monkeypatch, bot, chat_id, mask_position, recwarn
|
||||
@@ -804,6 +828,12 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
|
||||
sticker_type=Sticker.MASK,
|
||||
)
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn[0].filename == __file__, "wrong stacklevel"
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert str(recwarn[0].message).startswith("Since Bot API 6.6, the parameters")
|
||||
assert "for `create_new_sticker_set` are deprecated" in str(recwarn[0].message)
|
||||
|
||||
recwarn.clear()
|
||||
monkeypatch.setattr(bot, "_post", make_assertion_new_params)
|
||||
await bot.create_new_sticker_set(
|
||||
chat_id,
|
||||
@@ -813,17 +843,22 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
|
||||
sticker_format=StickerFormat.STATIC,
|
||||
needs_repainting=True,
|
||||
)
|
||||
assert len(recwarn) == 1
|
||||
assert len(recwarn) == 0
|
||||
|
||||
async def test_add_sticker_to_set_warning(self, bot, monkeypatch, chat_id, recwarn):
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_add_sticker_to_set_warning(
|
||||
self, bot, raw_bot, monkeypatch, bot_class, chat_id, recwarn
|
||||
):
|
||||
async def make_assertion(*args, **kwargs):
|
||||
return True
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
monkeypatch.setattr(bot, "_post", make_assertion)
|
||||
|
||||
await bot.add_sticker_to_set(chat_id, "name", "emoji", "fake_file_id")
|
||||
assert len(recwarn) == 1
|
||||
assert "Since Bot API 6.6, the parameters" in str(recwarn[0].message)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__
|
||||
|
||||
async def test_add_sticker_to_set_missing_required_arg(self, bot, chat_id):
|
||||
@@ -872,7 +907,13 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
|
||||
chat_id, "name", sticker=InputSticker(sticker=file, emoji_list=["this"])
|
||||
)
|
||||
assert test_flag
|
||||
assert len(recwarn) in (1, 2)
|
||||
|
||||
warnings = [w for w in recwarn if w.category is not ResourceWarning]
|
||||
assert len(warnings) == 1
|
||||
assert warnings[0].category is PTBDeprecationWarning
|
||||
assert warnings[0].filename == __file__
|
||||
assert str(warnings[0].message).startswith("Since Bot API 6.6, the parameters")
|
||||
assert "for `add_sticker_to_set` are deprecated" in str(warnings[0].message)
|
||||
|
||||
@pytest.mark.parametrize("local_mode", [True, False])
|
||||
async def test_set_sticker_set_thumbnail_local_files(
|
||||
@@ -898,34 +939,26 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
|
||||
finally:
|
||||
bot._local_mode = False
|
||||
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_set_sticker_set_thumb_deprecation_warning(
|
||||
self, monkeypatch, bot, raw_bot, recwarn
|
||||
self, monkeypatch, bot, raw_bot, recwarn, bot_class
|
||||
):
|
||||
ext_bot = bot
|
||||
bot = bot if bot_class == "ExtBot" else raw_bot
|
||||
|
||||
async def _post(*args, **kwargs):
|
||||
return True
|
||||
|
||||
for bot in (ext_bot, raw_bot):
|
||||
cls_name = bot.__class__.__name__
|
||||
monkeypatch.setattr(bot, "_post", _post)
|
||||
await bot.set_sticker_set_thumb("name", "user_id", "thumb")
|
||||
monkeypatch.setattr(bot, "_post", _post)
|
||||
await bot.set_sticker_set_thumb("name", "user_id", "thumb")
|
||||
|
||||
if isinstance(bot, ExtBot):
|
||||
# Unfortunately, warnings.catch_warnings doesn't play well with pytest apparently
|
||||
assert len(recwarn) == 2, f"Wrong warning number for class {cls_name}!"
|
||||
else:
|
||||
assert len(recwarn) == 1, f"No warning for class {cls_name}!"
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert "renamed the method 'setStickerSetThumb' to 'setStickerSetThumbnail'" in str(
|
||||
recwarn[0].message
|
||||
)
|
||||
|
||||
assert (
|
||||
recwarn[0].category is PTBDeprecationWarning
|
||||
), f"Wrong warning for class {cls_name}!"
|
||||
assert "renamed the method 'setStickerSetThumb' to 'setStickerSetThumbnail'" in str(
|
||||
recwarn[0].message
|
||||
), f"Wrong message for class {cls_name}!"
|
||||
|
||||
assert recwarn[0].filename == __file__, f"incorrect stacklevel for class {cls_name}!"
|
||||
recwarn.clear()
|
||||
assert recwarn[0].filename == __file__, "incorrect stacklevel!"
|
||||
recwarn.clear()
|
||||
|
||||
async def test_get_file_instance_method(self, monkeypatch, sticker):
|
||||
async def make_assertion(*_, **kwargs):
|
||||
@@ -985,17 +1018,22 @@ class TestStickerSetWithRequest:
|
||||
assert v
|
||||
|
||||
async def test_delete_sticker_set(self, bot, chat_id, sticker_file):
|
||||
try:
|
||||
# try creating a new sticker set - just in case the last deletion test failed
|
||||
assert await bot.create_new_sticker_set(
|
||||
chat_id,
|
||||
name=f"temp_set_by_{bot.username}",
|
||||
title="Stickerset delete Test",
|
||||
stickers=[InputSticker(sticker_file, emoji_list=["😄"])],
|
||||
sticker_format=StickerFormat.STATIC,
|
||||
)
|
||||
finally:
|
||||
assert await bot.delete_sticker_set(f"temp_set_by_{bot.username}")
|
||||
# there is currently an issue in the API where this function claims it successfully
|
||||
# creates an already deleted sticker set while it does not. This happens when calling it
|
||||
# too soon after deleting the set. This then leads to delete_sticker_set failing since the
|
||||
# pack does not exist. Making the name random prevents this issue.
|
||||
name = f"{''.join(random.choices(string.ascii_lowercase, k=5))}_temp_set_by_{bot.username}"
|
||||
assert await bot.create_new_sticker_set(
|
||||
chat_id,
|
||||
name=name,
|
||||
title="Stickerset delete Test",
|
||||
stickers=[InputSticker(sticker_file, emoji_list=["😄"])],
|
||||
sticker_format=StickerFormat.STATIC,
|
||||
)
|
||||
# this prevents a second issue when calling delete too soon after creating the set leads
|
||||
# to it failing as well
|
||||
await asyncio.sleep(1)
|
||||
assert await bot.delete_sticker_set(name)
|
||||
|
||||
async def test_set_custom_emoji_sticker_set_thumbnail(
|
||||
self, bot, chat_id, animated_sticker_file
|
||||
|
||||
@@ -144,7 +144,7 @@ class TestVenueWithoutRequest(TestVenueBase):
|
||||
|
||||
class TestVenueWithRequest(TestVenueBase):
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
|
||||
+31
-15
@@ -31,12 +31,15 @@ from tests.auxil.bot_method_checks import (
|
||||
check_shortcut_call,
|
||||
check_shortcut_signature,
|
||||
)
|
||||
from tests.auxil.deprecations import check_thumb_deprecation_warnings_for_args_and_attrs
|
||||
from tests.auxil.deprecations import (
|
||||
check_thumb_deprecation_warning_for_method_args,
|
||||
check_thumb_deprecation_warnings_for_args_and_attrs,
|
||||
)
|
||||
from tests.auxil.files import data_file
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def video_file():
|
||||
with data_file("telegram.mp4").open("rb") as f:
|
||||
yield f
|
||||
@@ -76,14 +79,14 @@ class TestVideoWithoutRequest(TestVideoBase):
|
||||
assert isinstance(video, Video)
|
||||
assert isinstance(video.file_id, str)
|
||||
assert isinstance(video.file_unique_id, str)
|
||||
assert video.file_id != ""
|
||||
assert video.file_unique_id != ""
|
||||
assert video.file_id
|
||||
assert video.file_unique_id
|
||||
|
||||
assert isinstance(video.thumbnail, PhotoSize)
|
||||
assert isinstance(video.thumbnail.file_id, str)
|
||||
assert isinstance(video.thumbnail.file_unique_id, str)
|
||||
assert video.thumbnail.file_id != ""
|
||||
assert video.thumbnail.file_unique_id != ""
|
||||
assert video.thumbnail.file_id
|
||||
assert video.thumbnail.file_unique_id
|
||||
|
||||
def test_expected_values(self, video):
|
||||
assert video.width == self.width
|
||||
@@ -171,6 +174,19 @@ class TestVideoWithoutRequest(TestVideoBase):
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
assert await bot.send_video(chat_id, video=video)
|
||||
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_send_video_thumb_deprecation_warning(
|
||||
self, recwarn, monkeypatch, bot_class, bot, raw_bot, chat_id, video
|
||||
):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
return True
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
await bot.send_video(chat_id, video, thumb="thumb")
|
||||
check_thumb_deprecation_warning_for_method_args(recwarn, __file__)
|
||||
|
||||
async def test_send_video_custom_filename(self, bot, chat_id, video_file, monkeypatch):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
return list(request_data.multipart_data.values())[0][0] == "custom_filename"
|
||||
@@ -244,8 +260,8 @@ class TestVideoWithRequest(TestVideoBase):
|
||||
assert isinstance(message.video, Video)
|
||||
assert isinstance(message.video.file_id, str)
|
||||
assert isinstance(message.video.file_unique_id, str)
|
||||
assert message.video.file_id != ""
|
||||
assert message.video.file_unique_id != ""
|
||||
assert message.video.file_id
|
||||
assert message.video.file_unique_id
|
||||
assert message.video.width == video.width
|
||||
assert message.video.height == video.height
|
||||
assert message.video.duration == video.duration
|
||||
@@ -282,8 +298,8 @@ class TestVideoWithRequest(TestVideoBase):
|
||||
assert isinstance(message.video, Video)
|
||||
assert isinstance(message.video.file_id, str)
|
||||
assert isinstance(message.video.file_unique_id, str)
|
||||
assert message.video.file_id != ""
|
||||
assert message.video.file_unique_id != ""
|
||||
assert message.video.file_id
|
||||
assert message.video.file_unique_id
|
||||
assert message.video.width == video.width
|
||||
assert message.video.height == video.height
|
||||
assert message.video.duration == video.duration
|
||||
@@ -292,8 +308,8 @@ class TestVideoWithRequest(TestVideoBase):
|
||||
assert isinstance(message.video.thumbnail, PhotoSize)
|
||||
assert isinstance(message.video.thumbnail.file_id, str)
|
||||
assert isinstance(message.video.thumbnail.file_unique_id, str)
|
||||
assert message.video.thumbnail.file_id != ""
|
||||
assert message.video.thumbnail.file_unique_id != ""
|
||||
assert message.video.thumbnail.file_id
|
||||
assert message.video.thumbnail.file_unique_id
|
||||
assert message.video.thumbnail.width == 51 # This seems odd that it's not self.thumb_width
|
||||
assert message.video.thumbnail.height == 90 # Ditto
|
||||
assert message.video.thumbnail.file_size == 645 # same
|
||||
@@ -358,7 +374,7 @@ class TestVideoWithRequest(TestVideoBase):
|
||||
assert not unprotected.has_protected_content
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -391,8 +407,8 @@ class TestVideoWithRequest(TestVideoBase):
|
||||
)
|
||||
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
await bot.send_video(chat_id, open(os.devnull, "rb"))
|
||||
with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError):
|
||||
await bot.send_video(chat_id, file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
|
||||
@@ -30,12 +30,15 @@ from tests.auxil.bot_method_checks import (
|
||||
check_shortcut_call,
|
||||
check_shortcut_signature,
|
||||
)
|
||||
from tests.auxil.deprecations import check_thumb_deprecation_warnings_for_args_and_attrs
|
||||
from tests.auxil.deprecations import (
|
||||
check_thumb_deprecation_warning_for_method_args,
|
||||
check_thumb_deprecation_warnings_for_args_and_attrs,
|
||||
)
|
||||
from tests.auxil.files import data_file
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def video_note_file():
|
||||
with data_file("telegram2.mp4").open("rb") as f:
|
||||
yield f
|
||||
@@ -70,14 +73,14 @@ class TestVideoNoteWithoutRequest(TestVideoNoteBase):
|
||||
assert isinstance(video_note, VideoNote)
|
||||
assert isinstance(video_note.file_id, str)
|
||||
assert isinstance(video_note.file_unique_id, str)
|
||||
assert video_note.file_id != ""
|
||||
assert video_note.file_unique_id != ""
|
||||
assert video_note.file_id
|
||||
assert video_note.file_unique_id
|
||||
|
||||
assert isinstance(video_note.thumbnail, PhotoSize)
|
||||
assert isinstance(video_note.thumbnail.file_id, str)
|
||||
assert isinstance(video_note.thumbnail.file_unique_id, str)
|
||||
assert video_note.thumbnail.file_id != ""
|
||||
assert video_note.thumbnail.file_unique_id != ""
|
||||
assert video_note.thumbnail.file_id
|
||||
assert video_note.thumbnail.file_unique_id
|
||||
|
||||
def test_expected_values(self, video_note):
|
||||
assert video_note.length == self.length
|
||||
@@ -149,6 +152,19 @@ class TestVideoNoteWithoutRequest(TestVideoNoteBase):
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
assert await bot.send_video_note(chat_id, video_note=video_note)
|
||||
|
||||
@pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"])
|
||||
async def test_send_video_note_thumb_deprecation_warning(
|
||||
self, recwarn, monkeypatch, bot_class, bot, raw_bot, chat_id, video_note
|
||||
):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
return True
|
||||
|
||||
bot = raw_bot if bot_class == "Bot" else bot
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
await bot.send_video_note(chat_id, video_note, thumb="thumb")
|
||||
check_thumb_deprecation_warning_for_method_args(recwarn, __file__)
|
||||
|
||||
async def test_send_video_note_custom_filename(
|
||||
self, bot, chat_id, video_note_file, monkeypatch
|
||||
):
|
||||
@@ -221,8 +237,8 @@ class TestVideoNoteWithRequest(TestVideoNoteBase):
|
||||
assert isinstance(message.video_note, VideoNote)
|
||||
assert isinstance(message.video_note.file_id, str)
|
||||
assert isinstance(message.video_note.file_unique_id, str)
|
||||
assert message.video_note.file_id != ""
|
||||
assert message.video_note.file_unique_id != ""
|
||||
assert message.video_note.file_id
|
||||
assert message.video_note.file_unique_id
|
||||
assert message.video_note.length == video_note.length
|
||||
assert message.video_note.duration == video_note.duration
|
||||
assert message.video_note.file_size == video_note.file_size
|
||||
@@ -252,7 +268,7 @@ class TestVideoNoteWithRequest(TestVideoNoteBase):
|
||||
assert message.video_note == video_note
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -295,8 +311,8 @@ class TestVideoNoteWithRequest(TestVideoNoteBase):
|
||||
assert not unprotected.has_protected_content
|
||||
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
await bot.send_video_note(chat_id, open(os.devnull, "rb"))
|
||||
with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError):
|
||||
await bot.send_video_note(chat_id, file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
|
||||
+10
-10
@@ -35,7 +35,7 @@ from tests.auxil.files import data_file
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@pytest.fixture()
|
||||
def voice_file():
|
||||
with data_file("telegram.ogg").open("rb") as f:
|
||||
yield f
|
||||
@@ -68,8 +68,8 @@ class TestVoiceWithoutRequest(TestVoiceBase):
|
||||
assert isinstance(voice, Voice)
|
||||
assert isinstance(voice.file_id, str)
|
||||
assert isinstance(voice.file_unique_id, str)
|
||||
assert voice.file_id != ""
|
||||
assert voice.file_unique_id != ""
|
||||
assert voice.file_id
|
||||
assert voice.file_unique_id
|
||||
|
||||
def test_expected_values(self, voice):
|
||||
assert voice.duration == self.duration
|
||||
@@ -191,8 +191,8 @@ class TestVoiceWithRequest(TestVoiceBase):
|
||||
assert isinstance(message.voice, Voice)
|
||||
assert isinstance(message.voice.file_id, str)
|
||||
assert isinstance(message.voice.file_unique_id, str)
|
||||
assert message.voice.file_id != ""
|
||||
assert message.voice.file_unique_id != ""
|
||||
assert message.voice.file_id
|
||||
assert message.voice.file_unique_id
|
||||
assert message.voice.duration == voice.duration
|
||||
assert message.voice.mime_type == voice.mime_type
|
||||
assert message.voice.file_size == voice.file_size
|
||||
@@ -220,8 +220,8 @@ class TestVoiceWithRequest(TestVoiceBase):
|
||||
assert isinstance(message.voice, Voice)
|
||||
assert isinstance(message.voice.file_id, str)
|
||||
assert isinstance(message.voice.file_unique_id, str)
|
||||
assert message.voice.file_id != ""
|
||||
assert message.voice.file_unique_id != ""
|
||||
assert message.voice.file_id
|
||||
assert message.voice.file_unique_id
|
||||
assert message.voice.duration == voice.duration
|
||||
assert message.voice.mime_type == voice.mime_type
|
||||
assert message.voice.file_size == voice.file_size
|
||||
@@ -285,7 +285,7 @@ class TestVoiceWithRequest(TestVoiceBase):
|
||||
assert not unprotected.has_protected_content
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"default_bot,custom",
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"allow_sending_without_reply": True}, None),
|
||||
({"allow_sending_without_reply": False}, None),
|
||||
@@ -318,8 +318,8 @@ class TestVoiceWithRequest(TestVoiceBase):
|
||||
)
|
||||
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
await bot.sendVoice(chat_id, open(os.devnull, "rb"))
|
||||
with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError):
|
||||
await bot.sendVoice(chat_id, file)
|
||||
|
||||
async def test_error_send_empty_file_id(self, bot, chat_id):
|
||||
with pytest.raises(TelegramError):
|
||||
|
||||
@@ -19,7 +19,13 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import CallbackGame, InlineKeyboardButton, LoginUrl, WebAppInfo
|
||||
from telegram import (
|
||||
CallbackGame,
|
||||
InlineKeyboardButton,
|
||||
LoginUrl,
|
||||
SwitchInlineQueryChosenChat,
|
||||
WebAppInfo,
|
||||
)
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@@ -35,6 +41,7 @@ def inline_keyboard_button():
|
||||
pay=TestInlineKeyboardButtonBase.pay,
|
||||
login_url=TestInlineKeyboardButtonBase.login_url,
|
||||
web_app=TestInlineKeyboardButtonBase.web_app,
|
||||
switch_inline_query_chosen_chat=TestInlineKeyboardButtonBase.switch_inline_query_chosen_chat, # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
@@ -48,6 +55,7 @@ class TestInlineKeyboardButtonBase:
|
||||
pay = True
|
||||
login_url = LoginUrl("http://google.com")
|
||||
web_app = WebAppInfo(url="https://example.com")
|
||||
switch_inline_query_chosen_chat = SwitchInlineQueryChosenChat("a_bot", True, False, True, True)
|
||||
|
||||
|
||||
class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase):
|
||||
@@ -70,6 +78,10 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase):
|
||||
assert inline_keyboard_button.pay == self.pay
|
||||
assert inline_keyboard_button.login_url == self.login_url
|
||||
assert inline_keyboard_button.web_app == self.web_app
|
||||
assert (
|
||||
inline_keyboard_button.switch_inline_query_chosen_chat
|
||||
== self.switch_inline_query_chosen_chat
|
||||
)
|
||||
|
||||
def test_to_dict(self, inline_keyboard_button):
|
||||
inline_keyboard_button_dict = inline_keyboard_button.to_dict()
|
||||
@@ -93,8 +105,12 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase):
|
||||
assert inline_keyboard_button_dict["pay"] == inline_keyboard_button.pay
|
||||
assert (
|
||||
inline_keyboard_button_dict["login_url"] == inline_keyboard_button.login_url.to_dict()
|
||||
) # NOQA: E127
|
||||
)
|
||||
assert inline_keyboard_button_dict["web_app"] == inline_keyboard_button.web_app.to_dict()
|
||||
assert (
|
||||
inline_keyboard_button_dict["switch_inline_query_chosen_chat"]
|
||||
== inline_keyboard_button.switch_inline_query_chosen_chat.to_dict()
|
||||
)
|
||||
|
||||
def test_de_json(self, bot):
|
||||
json_dict = {
|
||||
@@ -107,6 +123,7 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase):
|
||||
"web_app": self.web_app.to_dict(),
|
||||
"login_url": self.login_url.to_dict(),
|
||||
"pay": self.pay,
|
||||
"switch_inline_query_chosen_chat": self.switch_inline_query_chosen_chat.to_dict(),
|
||||
}
|
||||
|
||||
inline_keyboard_button = InlineKeyboardButton.de_json(json_dict, None)
|
||||
@@ -124,6 +141,10 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase):
|
||||
assert inline_keyboard_button.pay == self.pay
|
||||
assert inline_keyboard_button.login_url == self.login_url
|
||||
assert inline_keyboard_button.web_app == self.web_app
|
||||
assert (
|
||||
inline_keyboard_button.switch_inline_query_chosen_chat
|
||||
== self.switch_inline_query_chosen_chat
|
||||
)
|
||||
|
||||
none = InlineKeyboardButton.de_json({}, bot)
|
||||
assert none is None
|
||||
|
||||
@@ -179,17 +179,17 @@ class TestInlineKeyboardMarkupWithoutRequest(TestInlineKeyboardMarkupBase):
|
||||
)
|
||||
|
||||
def test_wrong_keyboard_inputs(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="should be a sequence of sequences"):
|
||||
InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("b1", "1")], InlineKeyboardButton("b2", "2")]
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="should be a sequence of sequences"):
|
||||
InlineKeyboardMarkup("strings_are_not_allowed")
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="should be a sequence of sequences"):
|
||||
InlineKeyboardMarkup(["strings_are_not_allowed_in_the_rows_either"])
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="should be a sequence of sequences"):
|
||||
InlineKeyboardMarkup(InlineKeyboardButton("b1", "1"))
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="should be a sequence of sequences"):
|
||||
InlineKeyboardMarkup([[[InlineKeyboardButton("only_2d_array_is_allowed", "1")]]])
|
||||
|
||||
async def test_expected_values_empty_switch(self, inline_keyboard_markup, bot, monkeypatch):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user