Compare commits

...

25 Commits

Author SHA1 Message Date
Hinrich Mahler bfbf6d3f94 Bump Version to v20.3 2023-05-07 15:31:23 +02:00
Bibo-Joshi 0c4180c74b Documentation Improvements (#3628, #3636, #3694)
Co-authored-by: Yossi Rafelson <yossi.rafelson@gmail.com>
Co-authored-by: Aditya <clot27@apx_managed.vanilla>
Co-authored-by: ibragimovgeorge <103066850+ibragimovgeorge@users.noreply.github.com>
2023-05-07 14:51:22 +02:00
Bibo-Joshi 1c6ae435bf Add a Stability Policy (#3622)
Co-authored-by: poolitzer <github@poolitzer.eu>
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com>
2023-05-07 14:50:41 +02:00
Bibo-Joshi 66b6d3c497 Recover 100% Type Completeness (#3676) 2023-05-07 14:10:20 +02:00
Bibo-Joshi 8c252c9822 API 6.7 (#3673)
Co-authored-by: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com>
Co-authored-by: Harshil Mehta <37377066+harshil21@users.noreply.github.com>
Co-authored-by: poolitzer <github@poolitzer.eu>
Co-authored-by: Aditya <clot27@apx_managed.vanilla>
2023-05-07 13:44:34 +02:00
Bibo-Joshi 450dc2115c Shield Update Fetcher Task in Application.start (#3657) 2023-05-06 21:10:12 +02:00
pre-commit-ci[bot] 87a6890900 pre-commit autoupdate (#3688) 2023-05-06 20:52:58 +02:00
Bibo-Joshi 83ab12c387 Stabilize test_delete_sticker_set (#3685)
Co-authored-by: poolitzer <github@poolitzer.eu>
2023-04-30 11:03:00 +02:00
Bibo-Joshi 3f444dad8d Improve Warning Categories & Stacklevels (#3674) 2023-04-27 22:36:04 +02:00
Bibo-Joshi f23315d08b Add Logging for Invalid JSON Data in BasePersistence.parse_json_payload (#3668) 2023-04-18 16:17:20 +02:00
Luca Bellanti 7b116be344 Localize Received datetime Objects According to Defaults.tzinfo (#3632) 2023-04-18 16:16:23 +02:00
dependabot[bot] 934e4c9bd4 Bump pytest from 7.2.2 to 7.3.1 (#3661)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2023-04-16 11:24:50 +02:00
dependabot[bot] 1966fb25c5 Update httpx requirement from ~=0.23.3 to ~=0.24.0 (#3660)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2023-04-16 11:20:36 +02:00
dependabot[bot] a333d8514a Bump sphinx-copybutton from 0.5.1 to 0.5.2 (#3662)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2023-04-16 11:15:06 +02:00
Bibo-Joshi b146c7131e Remove Deprecated codecov Package from CI (#3664) 2023-04-16 10:54:49 +02:00
Bibo-Joshi 53093ebceb Give Loggers Better Names (#3623) 2023-04-10 17:01:35 +02:00
Luca Bellanti 401b2decce Make Message.link Point to Thread View Where Possible (#3640) 2023-04-07 17:13:45 +02:00
pre-commit-ci[bot] 83a164e5ef pre-commit autoupdate (#3646)
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
Co-authored-by: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com>
Co-authored-by: Harshil Mehta <37377066+harshil21@users.noreply.github.com>
2023-04-05 20:52:52 +02:00
Bibo-Joshi d91bc45cdc Add Application.mark_data_for_update_persistence (#3607) 2023-04-02 22:29:16 +02:00
dependabot[bot] 8967912f46 Bump furo from 2023.3.23 to 2023.3.27 (#3643) 2023-04-02 21:42:57 +02:00
dependabot[bot] 7ab2cafbee Bump actions/stale from 7 to 8 (#3644) 2023-04-02 21:41:37 +02:00
Harshil 7e0ed2235e Stabilize CI by Rerunning Failed Tests (#3631)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2023-04-01 22:35:20 +02:00
Harshil 53abb7b4bd Add String Representation for RequestParameter (#3634) 2023-03-27 22:03:02 +02:00
Harshil 11f86b8813 Drop Usage of sys.maxunicode (#3630) 2023-03-26 21:58:23 +02:00
Harshil 9997a9f47e Empower ruff (#3594) 2023-03-25 19:18:04 +01:00
180 changed files with 3554 additions and 1433 deletions
+51 -4
View File
@@ -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
+3 -35
View File
@@ -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`
+1 -1
View File
@@ -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
+9 -9
View File
@@ -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)
+6 -6
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
==========
+2 -2
View File
@@ -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
View File
@@ -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"
+1 -1
View File
@@ -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>`__
+4
View File
@@ -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
+4 -3
View File
@@ -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
+143
View File
@@ -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
+2
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
BotName
=======
.. autoclass:: telegram.BotName
:members:
:show-inheritance:
+2
View File
@@ -1,6 +1,8 @@
telegram.ext package
====================
.. automodule:: telegram.ext
.. toctree::
:titlesonly:
+1
View File
@@ -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:
+2
View File
@@ -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.
+6 -7
View File
@@ -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:
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
+2 -3
View File
@@ -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
+7
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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,)
+54
View File
@@ -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`"""
+11 -2
View File
@@ -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)
+12 -3
View File
@@ -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
View File
@@ -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",)
+22 -2
View File
@@ -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)
+1 -1
View File
@@ -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
-2
View File
@@ -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:
-4
View File
@@ -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]
+44
View File
@@ -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)
+3
View File
@@ -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`"""
+1 -2
View File
@@ -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
View File
@@ -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"![{escaped_text}](tg://emoji?id={custom_emoji_id})"
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.
"""
+2 -2
View File
@@ -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):
+1 -1
View File
@@ -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
View File
@@ -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]
+100
View File
@@ -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()
+9 -3
View File
@@ -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
View File
@@ -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
+23 -6
View File
@@ -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:
+49
View File
@@ -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)
+1 -1
View File
@@ -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[
+25 -16
View File
@@ -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,
)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+18 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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>`
"""
+9 -9
View File
@@ -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]
+91 -50
View File
@@ -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
+4 -3
View File
@@ -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.
+15
View File
@@ -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
+1 -1
View File
@@ -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 = (
+26 -27
View File
@@ -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)
+5 -5
View File
@@ -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
View File
@@ -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
+16 -1
View File
@@ -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,
+1 -1
View File
@@ -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::
+4 -1
View File
@@ -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
+2 -8
View File
@@ -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
View File
@@ -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
+4 -3
View File
@@ -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,
)
+17 -17
View File
@@ -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",
+5 -5
View File
@@ -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
View File
@@ -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}"
+5 -4
View File
@@ -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
+4 -4
View File
@@ -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()
+1 -1
View File
@@ -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.
+26 -12
View File
@@ -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):
+21 -7
View File
@@ -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):
+4 -5
View File
@@ -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):
+1 -1
View File
@@ -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),
+27 -12
View File
@@ -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),
+2 -3
View File
@@ -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):
+12 -11
View File
@@ -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)
+4 -2
View File
@@ -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()
+7 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View 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
View File
@@ -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):
+27 -11
View File
@@ -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
View File
@@ -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):
+23 -2
View File
@@ -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
+5 -5
View File
@@ -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