mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-19 15:45:13 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0015fcdf9d | |||
| 54f80cb54f | |||
| 4bc2f1f60b | |||
| 4f7f1ba21b | |||
| 9c0efaf2f6 | |||
| 8434c0aaea | |||
| 96138510ca | |||
| 6e951a9fd3 | |||
| 1f5cfc8f9b | |||
| 066ba5bb32 | |||
| 0375b7d701 | |||
| 67db3df426 | |||
| 152269cdcd | |||
| 8043bf265d | |||
| 14f8e89cef | |||
| 0f7d9ec5da | |||
| 047844f9af | |||
| caceaf71b8 | |||
| cfa9230f15 | |||
| b37ba3a44e | |||
| 1116de4ebd | |||
| 437c4d7bfb | |||
| 630db8f0ef | |||
| 008fcdea5f | |||
| 7aa1356089 | |||
| ecfb4583d6 | |||
| f56371fae3 | |||
| 9f3c5e4f2b | |||
| 489561d531 | |||
| 568e63933c | |||
| f8f12e7bd4 | |||
| 94afda2b69 | |||
| a81fc86792 |
+16
-20
@@ -8,7 +8,7 @@
|
||||
],
|
||||
|
||||
// Add pull request labels:
|
||||
"labels": ["dependencies"],
|
||||
"labels": ["⚙️ ci-cd"],
|
||||
|
||||
// Bump even patch versions:
|
||||
"bumpVersion": "patch",
|
||||
@@ -19,24 +19,12 @@
|
||||
// Update the lock files:
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["* * * * 1,4"] // Run sometime on Monday and Thursday
|
||||
"schedule": ["* 0-3 1 * *"], // https://docs.renovatebot.com/presets-schedule/#schedulemonthly
|
||||
"automerge": true
|
||||
},
|
||||
|
||||
// Bump the versions even in other files:
|
||||
"bumpVersions": [
|
||||
{
|
||||
"name": "Update dependency versions in README.rst",
|
||||
"filePatterns": ["README.rst"],
|
||||
"matchStrings": [
|
||||
"cryptography>=(?<version>\\d+\\.\\d+\\.\\d+)",
|
||||
"aiolimiter~=(?<version>\\d+\\.\\d+\\.\\d+)",
|
||||
"tornado~=(?<version>\\d+\\.\\d+)",
|
||||
"cachetools>=(?<version>\\d+\\.\\d+\\.\\d+)", // Lower bound only
|
||||
"APScheduler>=(?<version>\\d+\\.\\d+\\.\\d+)" // Lower bound only
|
||||
],
|
||||
"bumpType": "minor"
|
||||
}
|
||||
],
|
||||
// Enable automerge globally:
|
||||
"automerge": true,
|
||||
|
||||
// Group package updates together:
|
||||
"packageRules": [
|
||||
@@ -62,10 +50,18 @@
|
||||
"matchPackageNames": ["chango", "Bibo-Joshi/chango"],
|
||||
"groupName": "Chango"
|
||||
},
|
||||
// Automerge PR's for minor/patch/pin/digest (except major) updates:
|
||||
|
||||
// Don't automerge major updates for project dependencies:
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"automerge": true
|
||||
"matchUpdateTypes": ["major"],
|
||||
"matchDepTypes": ["project.dependencies", "project.optional-dependencies"],
|
||||
"automerge": false
|
||||
},
|
||||
|
||||
// Apply the "dependencies" label to all updates of optional/required dependencies:
|
||||
{
|
||||
"matchDepTypes": ["project.optional-dependencies", "project.dependencies"],
|
||||
"labels": ["⚙️ dependencies"]
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Create the new fragment
|
||||
- uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # 0.4.0
|
||||
- uses: Bibo-Joshi/chango@212fc662da1b1026f335e110270d75690df05758 # 0.5.0
|
||||
with:
|
||||
github-token: ${{ secrets.CHANGO_PAT }}
|
||||
query-issue-types: true
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
# Run `chango release` if applicable - needs some additional setup.
|
||||
- name: Set up Python
|
||||
if: steps.check_title.outputs.IS_RELEASE_PR == 'true'
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
with:
|
||||
# Install a specific version of uv.
|
||||
version: "0.8.3"
|
||||
version: "0.8.22"
|
||||
# Install 3.13:
|
||||
python-version: 3.13
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -21,13 +21,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --persona=pedantic --format sarif . > results.sarif
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
|
||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install pypa/build
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
|
||||
compute-signatures:
|
||||
name: Compute SHA1 Sums and Sign with Sigstore
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install pypa/build
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
# For adding labels and closing
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||
with:
|
||||
# PRs never get stale
|
||||
days-before-stale: 3
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
.test_report_optionals_junit.xml
|
||||
|
||||
- name: Submit coverage
|
||||
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
env_vars: OS,PYTHON
|
||||
name: ${{ matrix.os }}-${{ matrix.python-version }}
|
||||
|
||||
@@ -8,7 +8,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.12.7'
|
||||
rev: 'v0.13.2'
|
||||
hooks:
|
||||
# Run the linter:
|
||||
- id: ruff-check
|
||||
@@ -17,7 +17,7 @@ repos:
|
||||
- id: ruff-format
|
||||
name: ruff format
|
||||
- repo: https://github.com/PyCQA/pylint
|
||||
rev: v3.3.7
|
||||
rev: v3.3.8
|
||||
hooks:
|
||||
- id: pylint
|
||||
files: ^(?!(tests|docs)).*\.py$
|
||||
@@ -25,12 +25,12 @@ repos:
|
||||
additional_dependencies:
|
||||
- httpx~=0.27
|
||||
- tornado~=6.4
|
||||
- APScheduler~=3.10.4
|
||||
- cachetools>=5.3.3,<5.5.0
|
||||
- APScheduler>=3.10.4,<3.12.0
|
||||
- cachetools>=5.3.3,<6.3.0
|
||||
- aiolimiter~=1.1,<1.3
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.16.1
|
||||
rev: v1.18.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy-ptb
|
||||
@@ -42,8 +42,8 @@ repos:
|
||||
- types-cachetools
|
||||
- httpx~=0.27
|
||||
- tornado~=6.4
|
||||
- APScheduler~=3.10.4
|
||||
- cachetools>=5.3.3,<5.5.0
|
||||
- APScheduler>=3.10.4,<3.12.0
|
||||
- cachetools>=5.3.3,<6.3.0
|
||||
- aiolimiter~=1.1,<1.3
|
||||
- . # this basically does `pip install -e .`
|
||||
- id: mypy
|
||||
@@ -55,6 +55,6 @@ repos:
|
||||
- --follow-imports=silent
|
||||
additional_dependencies:
|
||||
- tornado~=6.4
|
||||
- APScheduler~=3.10.4
|
||||
- cachetools>=5.3.3,<5.5.0
|
||||
- APScheduler>=3.10.4,<3.12.0
|
||||
- cachetools>=5.3.3,<6.3.0
|
||||
- . # this basically does `pip install -e .`
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
features = """
|
||||
Add convenience methods for ``BusinessOpeningHours``:
|
||||
|
||||
* ``get_opening_hours_for_day``: returns the opening hours applicable for a specific date
|
||||
* ``is_open``: indicates whether the business is open at the specified date and time.
|
||||
"""
|
||||
[[pull_requests]]
|
||||
uid = "4861"
|
||||
author_uid = "Aweryc"
|
||||
closes_threads = ["4194"]
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Lock file maintenance"
|
||||
[[pull_requests]]
|
||||
uid = "4938"
|
||||
author_uid = "renovate[bot]"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update astral-sh/setup-uv digest to b75a909"
|
||||
[[pull_requests]]
|
||||
uid = "4943"
|
||||
author_uid = "renovate[bot]"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update codecov/codecov-action action to v5.5.1"
|
||||
[[pull_requests]]
|
||||
uid = "4944"
|
||||
author_uid = "renovate[bot]"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update dependency astral-sh/uv to v0.8.17"
|
||||
[[pull_requests]]
|
||||
uid = "4945"
|
||||
author_uid = "renovate[bot]"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update github/codeql-action action to v3.30.3"
|
||||
[[pull_requests]]
|
||||
uid = "4946"
|
||||
author_uid = "renovate[bot]"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update Pylint to v3.3.8"
|
||||
[[pull_requests]]
|
||||
uid = "4947"
|
||||
author_uid = "renovate[bot]"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update Chango to v0.5.0"
|
||||
[[pull_requests]]
|
||||
uid = "4948"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update Mypy to v1.18.1"
|
||||
[[pull_requests]]
|
||||
uid = "4949"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Align pre-commit hook APScheduler to with ``pyproject.toml``"
|
||||
[[pull_requests]]
|
||||
uid = "4950"
|
||||
author_uids = ["renovate[bot]", "Bibo-Joshi"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Align pre-commit hook cachetools to with ``pyproject.toml``"
|
||||
[[pull_requests]]
|
||||
uid = "4951"
|
||||
author_uids = ["renovate[bot]", "Bibo-Joshi"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update pypa/gh-action-pypi-publish action to v1.13.0"
|
||||
[[pull_requests]]
|
||||
uid = "4952"
|
||||
author_uid = "renovate[bot]"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Renovate: No README updates, label behaviour change, automerge lockfiles"
|
||||
[[pull_requests]]
|
||||
uid = "4953"
|
||||
author_uid = "harshil21"
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Lock file maintenance"
|
||||
[[pull_requests]]
|
||||
uid = "4954"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Lock file maintenance"
|
||||
[[pull_requests]]
|
||||
uid = "4955"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update astral-sh/setup-uv digest to 208b0c0"
|
||||
[[pull_requests]]
|
||||
uid = "4958"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update dependency astral-sh/uv to v0.8.19"
|
||||
[[pull_requests]]
|
||||
uid = "4959"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update Mypy to v1.18.2"
|
||||
[[pull_requests]]
|
||||
uid = "4960"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update astral-sh/setup-uv action to v6.7.0"
|
||||
[[pull_requests]]
|
||||
uid = "4961"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update Ruff to v0.13.1"
|
||||
[[pull_requests]]
|
||||
uid = "4962"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update actions/stale action to v10"
|
||||
[[pull_requests]]
|
||||
uid = "4964"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Properly Pin Dependency to ``astral/setup-uv`` in Copilot Setup Steps"
|
||||
[[pull_requests]]
|
||||
uid = "4965"
|
||||
author_uids = ["Bibo-Joshi"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Lock file maintenance"
|
||||
[[pull_requests]]
|
||||
uid = "4967"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Tune Renovate Configuration"
|
||||
[[pull_requests]]
|
||||
uid = "4968"
|
||||
author_uids = ["harshil21"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,10 @@
|
||||
breaking = """Move param ``ReplyParameters.checklist_task_id`` to last position.
|
||||
|
||||
.. hint::
|
||||
This change addresses a breaking change accidentally introduced in version 22.4 where the introduction of the new parameter was not done in a backward compatible way.
|
||||
Existing code using keyword arguments is unaffected. Only code using positional arguments (and based on version 22.4) may need updates.
|
||||
"""
|
||||
[[pull_requests]]
|
||||
uid = "4972"
|
||||
author_uids = ["aelkheir"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
bugfixes = "Fix Handling of Infinite Bootstrap Retries in ``Application.run_*`` and ``Updater.start_*``"
|
||||
[[pull_requests]]
|
||||
uid = "4973"
|
||||
author_uids = ["Bibo-Joshi"]
|
||||
closes_threads = ["4966"]
|
||||
@@ -0,0 +1,4 @@
|
||||
documentation = "Documentation Improvemennts"
|
||||
[[pull_requests]]
|
||||
uid = "4974"
|
||||
author_uids = ["Bibo-Joshi"]
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update dependency astral-sh/uv to v0.8.22"
|
||||
[[pull_requests]]
|
||||
uid = "4975"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update github/codeql-action action to v3.30.5"
|
||||
[[pull_requests]]
|
||||
uid = "4976"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update Ruff to v0.13.2"
|
||||
[[pull_requests]]
|
||||
uid = "4977"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
internal = "Update dependency furo to v2025.9.25"
|
||||
[[pull_requests]]
|
||||
uid = "4978"
|
||||
author_uids = ["renovate[bot]"]
|
||||
closes_threads = []
|
||||
@@ -0,0 +1,5 @@
|
||||
other = "Bump Version to v22.5"
|
||||
[[pull_requests]]
|
||||
uid = "4979"
|
||||
author_uids = ["Bibo-Joshi"]
|
||||
closes_threads = []
|
||||
+5
-5
@@ -116,9 +116,9 @@ tests = [
|
||||
"pytest-cov"
|
||||
]
|
||||
docs = [
|
||||
"chango~=0.4.0; python_version >= '3.12'",
|
||||
"chango~=0.5.0; python_version >= '3.12'",
|
||||
"sphinx==8.2.3; python_version >= '3.11'",
|
||||
"furo==2025.7.19",
|
||||
"furo==2025.9.25",
|
||||
"sphinx-paramlinks==0.6.0",
|
||||
"sphinxcontrib-mermaid==1.0.0",
|
||||
"sphinx-copybutton==0.5.2",
|
||||
@@ -133,9 +133,9 @@ docs = [
|
||||
]
|
||||
linting = [
|
||||
"pre-commit",
|
||||
"ruff==0.12.7",
|
||||
"mypy==1.16.1",
|
||||
"pylint==3.3.7"
|
||||
"ruff==0.13.2",
|
||||
"mypy==1.18.2",
|
||||
"pylint==3.3.8"
|
||||
]
|
||||
all = [{ include-group = "tests" }, { include-group = "docs" }, { include-group = "linting"}]
|
||||
|
||||
|
||||
+113
-5
@@ -21,15 +21,24 @@
|
||||
|
||||
import datetime as dtm
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from telegram._chat import Chat
|
||||
from telegram._files.location import Location
|
||||
from telegram._files.sticker import Sticker
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.argumentparsing import (
|
||||
de_json_optional,
|
||||
de_list_optional,
|
||||
parse_sequence_arg,
|
||||
)
|
||||
from telegram._utils.datetime import (
|
||||
extract_tzinfo_from_defaults,
|
||||
from_timestamp,
|
||||
get_zone_info,
|
||||
)
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -449,7 +458,7 @@ class BusinessOpeningHoursInterval(TelegramObject):
|
||||
|
||||
Examples:
|
||||
A day has (24 * 60 =) 1440 minutes, a week has (7 * 1440 =) 10080 minutes.
|
||||
Starting the the minute's sequence from Monday, example values of
|
||||
Starting the minute's sequence from Monday, example values of
|
||||
:attr:`opening_minute`, :attr:`closing_minute` will map to the following day times:
|
||||
|
||||
* Monday - 8am to 8:30pm:
|
||||
@@ -552,7 +561,7 @@ class BusinessOpeningHours(TelegramObject):
|
||||
time intervals describing business opening hours.
|
||||
"""
|
||||
|
||||
__slots__ = ("opening_hours", "time_zone_name")
|
||||
__slots__ = ("_cached_zone_info", "opening_hours", "time_zone_name")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -567,10 +576,109 @@ class BusinessOpeningHours(TelegramObject):
|
||||
opening_hours
|
||||
)
|
||||
|
||||
self._cached_zone_info: Optional[ZoneInfo] = None
|
||||
|
||||
self._id_attrs = (self.time_zone_name, self.opening_hours)
|
||||
|
||||
self._freeze()
|
||||
|
||||
@property
|
||||
def _zone_info(self) -> ZoneInfo:
|
||||
if self._cached_zone_info is None:
|
||||
self._cached_zone_info = get_zone_info(self.time_zone_name)
|
||||
return self._cached_zone_info
|
||||
|
||||
def get_opening_hours_for_day(
|
||||
self, date: dtm.date, time_zone: Union[dtm.tzinfo, str, None] = None
|
||||
) -> tuple[tuple[dtm.datetime, dtm.datetime], ...]:
|
||||
"""Returns the opening hours intervals for a specific day as datetime objects.
|
||||
|
||||
.. versionadded:: 22.5
|
||||
|
||||
Args:
|
||||
date (:obj:`datetime.date`): The date to get opening hours for.
|
||||
time_zone (:obj:`datetime.tzinfo` | :obj:`str`, optional): Timezone to use for the
|
||||
returned datetime objects. If not specified, then :attr:`time_zone_name` be used.
|
||||
|
||||
Returns:
|
||||
tuple[tuple[:obj:`datetime.datetime`, :obj:`datetime.datetime`], ...]:
|
||||
A tuple of datetime pairs representing opening and closing times for the specified day.
|
||||
Each pair consists of ``(opening_time, closing_time)``.
|
||||
Returns an empty tuple if there are no opening hours for the given day.
|
||||
"""
|
||||
|
||||
week_day = date.weekday()
|
||||
res = []
|
||||
if isinstance(time_zone, str):
|
||||
tz_target: dtm.tzinfo = get_zone_info(time_zone)
|
||||
elif time_zone is None:
|
||||
tz_target = self._zone_info
|
||||
else:
|
||||
tz_target = time_zone
|
||||
|
||||
for interval in self.opening_hours:
|
||||
int_open = interval.opening_time
|
||||
int_close = interval.closing_time
|
||||
|
||||
if int_open[0] != week_day:
|
||||
continue
|
||||
|
||||
# To get the correct localization, we first need to create the dtm object in
|
||||
# self.time_zone_name, then convert it to the target timezone. We could check if
|
||||
# self._zone_info == tz_target and skip the conversion, but it's not worth the added
|
||||
# complexity.
|
||||
result_int_open = dtm.datetime(
|
||||
year=date.year,
|
||||
month=date.month,
|
||||
day=date.day,
|
||||
hour=int_open[1],
|
||||
minute=int_open[2],
|
||||
tzinfo=self._zone_info,
|
||||
).astimezone(tz_target)
|
||||
|
||||
result_int_close = dtm.datetime(
|
||||
year=date.year,
|
||||
month=date.month,
|
||||
day=date.day,
|
||||
hour=int_close[1],
|
||||
minute=int_close[2],
|
||||
tzinfo=self._zone_info,
|
||||
).astimezone(tz_target)
|
||||
|
||||
res.append((result_int_open, result_int_close))
|
||||
|
||||
# The sorting is currently an implementation detail
|
||||
return tuple(sorted(res, key=lambda x: x[0]))
|
||||
|
||||
def is_open(self, datetime: dtm.datetime) -> bool:
|
||||
"""Check if the business is open at the specified datetime.
|
||||
|
||||
.. versionadded:: 22.5
|
||||
|
||||
Args:
|
||||
datetime (:obj:`datetime.datetime`): The datetime to check.
|
||||
If the object is timezone-naive, it is assumed to be in the
|
||||
timezone specified by :attr:`time_zone_name`.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: True if the business is open at the specified time, False otherwise.
|
||||
"""
|
||||
|
||||
datetime_in_native_tz = (
|
||||
datetime.replace(tzinfo=self._zone_info) if datetime.tzinfo is None else datetime
|
||||
).astimezone(self._zone_info)
|
||||
minute_of_week = (
|
||||
datetime_in_native_tz.weekday() * 1440
|
||||
+ datetime_in_native_tz.hour * 60
|
||||
+ datetime_in_native_tz.minute
|
||||
)
|
||||
|
||||
for interval in self.opening_hours:
|
||||
if interval.opening_minute <= minute_of_week < interval.closing_minute:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessOpeningHours":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
|
||||
@@ -377,6 +377,12 @@ class ReplyParameters(TelegramObject):
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. versionchanged:: 22.5
|
||||
The :paramref:`checklist_task_id` parameter has been moved to the last position to
|
||||
maintain backward compatibility with versions prior to 22.4.
|
||||
This resolves a breaking change accidentally introduced in version 22.4. See the changelog
|
||||
for version 22.5 for more information.
|
||||
|
||||
Args:
|
||||
message_id (:obj:`int`): Identifier of the message that will be replied to in the current
|
||||
chat, or in the chat :paramref:`chat_id` if it is specified.
|
||||
@@ -449,11 +455,11 @@ class ReplyParameters(TelegramObject):
|
||||
message_id: int,
|
||||
chat_id: Optional[Union[int, str]] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
checklist_task_id: Optional[int] = None,
|
||||
quote: Optional[str] = None,
|
||||
quote_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
quote_entities: Optional[Sequence[MessageEntity]] = None,
|
||||
quote_position: Optional[int] = None,
|
||||
checklist_task_id: Optional[int] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
|
||||
@@ -38,6 +38,7 @@ if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
Tele_co = TypeVar("Tele_co", bound="TelegramObject", covariant=True)
|
||||
Tele = TypeVar("Tele", bound="TelegramObject")
|
||||
|
||||
|
||||
class TelegramObject:
|
||||
@@ -457,7 +458,7 @@ class TelegramObject:
|
||||
return tuple(cls.de_json(d, bot) for d in data)
|
||||
|
||||
@contextmanager
|
||||
def _unfrozen(self: Tele_co) -> Iterator[Tele_co]:
|
||||
def _unfrozen(self: Tele) -> Iterator[Tele]:
|
||||
"""Context manager to temporarily unfreeze the object. For internal use only.
|
||||
|
||||
Note:
|
||||
|
||||
@@ -32,6 +32,7 @@ import contextlib
|
||||
import datetime as dtm
|
||||
import os
|
||||
import time
|
||||
import zoneinfo
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from telegram._utils.warnings import warn
|
||||
@@ -231,6 +232,21 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
|
||||
return dt_obj.timestamp()
|
||||
|
||||
|
||||
def get_zone_info(tz: str) -> zoneinfo.ZoneInfo:
|
||||
"""Wrapper around the `ZoneInfo` constructor with slightly more helpful error message
|
||||
in case tzdata is not installed.
|
||||
"""
|
||||
try:
|
||||
return zoneinfo.ZoneInfo(tz)
|
||||
except zoneinfo.ZoneInfoNotFoundError as err:
|
||||
raise zoneinfo.ZoneInfoNotFoundError(
|
||||
f"No time zone found with key {tz}. "
|
||||
"Make sure to use a valid time zone name and "
|
||||
f"correctly install the tzdata (https://pypi.org/project/tzdata/) package if "
|
||||
"your system does not provide the time zone data."
|
||||
) from err
|
||||
|
||||
|
||||
def get_timedelta_value(
|
||||
value: Optional[dtm.timedelta], attribute: str
|
||||
) -> Optional[Union[int, dtm.timedelta]]:
|
||||
|
||||
@@ -51,6 +51,6 @@ class Version(NamedTuple):
|
||||
|
||||
|
||||
__version_info__: Final[Version] = Version(
|
||||
major=22, minor=4, micro=0, releaselevel="final", serial=0
|
||||
major=22, minor=5, micro=0, releaselevel="final", serial=0
|
||||
)
|
||||
__version__: Final[str] = str(__version_info__)
|
||||
|
||||
@@ -116,8 +116,8 @@ class ApplicationHandlerStop(Exception):
|
||||
|
||||
|
||||
class Application(
|
||||
Generic[BT, CCT, UD, CD, BD, JQ],
|
||||
contextlib.AbstractAsyncContextManager["Application"],
|
||||
Generic[BT, CCT, UD, CD, BD, JQ],
|
||||
):
|
||||
"""This class dispatches all kinds of updates to its registered handlers, and is the entry
|
||||
point to a PTB application.
|
||||
|
||||
@@ -54,7 +54,7 @@ class PersistenceInput(NamedTuple):
|
||||
callback_data: bool = True
|
||||
|
||||
|
||||
class BasePersistence(Generic[UD, CD, BD], ABC):
|
||||
class BasePersistence(ABC, Generic[UD, CD, BD]):
|
||||
"""Interface class for adding persistence to your bot.
|
||||
Subclass this object for different implementations of a persistent bot.
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
|
||||
if isinstance(obj, CallbackQuery):
|
||||
self.callback_data_cache.process_callback_query(obj)
|
||||
return obj # type: ignore[return-value]
|
||||
return obj
|
||||
|
||||
if isinstance(obj, Message):
|
||||
if obj.reply_to_message:
|
||||
@@ -595,7 +595,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
|
||||
# Finally, handle the message itself
|
||||
self.callback_data_cache.process_message(message=obj)
|
||||
return obj # type: ignore[return-value]
|
||||
return obj
|
||||
|
||||
if isinstance(obj, ChatFullInfo) and obj.pinned_message:
|
||||
self.callback_data_cache.process_message(obj.pinned_message)
|
||||
|
||||
@@ -33,7 +33,7 @@ RT = TypeVar("RT")
|
||||
UT = TypeVar("UT")
|
||||
|
||||
|
||||
class BaseHandler(Generic[UT, CCT, RT], ABC):
|
||||
class BaseHandler(ABC, Generic[UT, CCT, RT]):
|
||||
"""The base class for all update handlers. Create custom handlers by inheriting from it.
|
||||
|
||||
Warning:
|
||||
|
||||
@@ -382,6 +382,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
interval=poll_interval,
|
||||
stop_event=self.__polling_task_stop_event,
|
||||
max_retries=-1,
|
||||
repeat_on_success=True,
|
||||
),
|
||||
name="Updater:start_polling:polling_task",
|
||||
)
|
||||
@@ -704,7 +705,6 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
# delete_webhook for polling
|
||||
if drop_pending_updates or not webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_del_webhook,
|
||||
description="Bootstrap delete Webhook",
|
||||
interval=bootstrap_interval,
|
||||
@@ -716,7 +716,6 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
# so we set it anyhow.
|
||||
if webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_set_webhook,
|
||||
description="Bootstrap Set Webhook",
|
||||
interval=bootstrap_interval,
|
||||
|
||||
@@ -51,12 +51,14 @@ async def network_retry_loop(
|
||||
stop_event: Optional[asyncio.Event] = None,
|
||||
is_running: Optional[Callable[[], bool]] = None,
|
||||
max_retries: int,
|
||||
repeat_on_success: bool = False,
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop in case of ``max_retries < 0``:
|
||||
* `is_running()` evaluates :obj:`False`
|
||||
* or `stop_event` is set.
|
||||
* `stop_event` is set.
|
||||
* calling `action_cb` succeeds and `repeat_on_success` is :obj:`False`.
|
||||
|
||||
Additional stop condition for loop in case of `max_retries >= 0``:
|
||||
* a call to `action_cb` succeeds
|
||||
@@ -87,8 +89,18 @@ async def network_retry_loop(
|
||||
* 0: No retries.
|
||||
* > 0: Number of retries.
|
||||
|
||||
repeat_on_success (:obj:`bool`): Whether to repeat the action after a successful call.
|
||||
Defaults to :obj:`False`.
|
||||
|
||||
Raises:
|
||||
ValueError: When passing `repeat_on_success=True` and `max_retries >= 0`. This case is
|
||||
currently not supported.
|
||||
|
||||
"""
|
||||
infinite_loop = max_retries < 0
|
||||
if repeat_on_success and max_retries >= 0: # pragma: no cover
|
||||
# This case here is only for completeness. It should not be used anywhere in the library.
|
||||
raise ValueError("Cannot use repeat_on_success=True with max_retries >= 0")
|
||||
|
||||
log_prefix = f"Network Retry Loop ({description}):"
|
||||
effective_is_running = is_running or (lambda: True)
|
||||
|
||||
@@ -120,7 +132,7 @@ async def network_retry_loop(
|
||||
while effective_is_running():
|
||||
try:
|
||||
await do_action()
|
||||
if not infinite_loop:
|
||||
if not repeat_on_success:
|
||||
_LOGGER.debug("%s Action succeeded. Stopping loop.", log_prefix)
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
|
||||
@@ -760,7 +760,7 @@ class TestSendMediaGroupWithoutRequest:
|
||||
):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="You can only supply either group caption or media with captions.",
|
||||
match="You can only supply either group caption or media with captions\\.",
|
||||
):
|
||||
await offline_bot.send_media_group(chat_id, group, caption="foo")
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ class TestPassportWithoutRequest(PassportTestBase):
|
||||
|
||||
# Different error messages for different cryptography versions
|
||||
with pytest.raises(
|
||||
ValueError, match="(Could not deserialize key data)|(Unable to load PEM file)"
|
||||
ValueError, match=r"(Could not deserialize key data)|(Unable to load PEM file)"
|
||||
):
|
||||
Bot(offline_bot.token, private_key=b"Invalid key!")
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import zoneinfo
|
||||
import pytest
|
||||
|
||||
from telegram._utils import datetime as tg_dtm
|
||||
from telegram._utils.datetime import get_zone_info
|
||||
from telegram.ext import Defaults
|
||||
|
||||
# sample time specification values categorised into absolute / delta / time-of-day
|
||||
@@ -138,7 +139,10 @@ class TestDatetime:
|
||||
# of an xpass when the test is run in a timezone with the same UTC offset
|
||||
ref_datetime = dtm.datetime(1970, 1, 1, 12)
|
||||
utc_offset = timezone.utcoffset(ref_datetime)
|
||||
ref_t, time_of_day = tg_dtm._datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
|
||||
ref_t, time_of_day = (
|
||||
tg_dtm._datetime_to_float_timestamp(ref_datetime),
|
||||
ref_datetime.time(),
|
||||
)
|
||||
aware_time_of_day = tg_dtm.localize(ref_datetime, timezone).timetz()
|
||||
|
||||
# first test that naive time is assumed to be utc:
|
||||
@@ -168,7 +172,7 @@ class TestDatetime:
|
||||
assert tg_dtm.to_timestamp(i) == int(tg_dtm.to_float_timestamp(i)), f"Failed for {i}"
|
||||
|
||||
def test_to_timestamp_none(self):
|
||||
# this 'convenience' behaviour has been left left for backwards compatibility
|
||||
# this 'convenience' behaviour has been left for backwards compatibility
|
||||
assert tg_dtm.to_timestamp(None) is None
|
||||
|
||||
def test_from_timestamp_none(self):
|
||||
@@ -193,6 +197,21 @@ class TestDatetime:
|
||||
assert tg_dtm.extract_tzinfo_from_defaults(bot) is None
|
||||
assert tg_dtm.extract_tzinfo_from_defaults(raw_bot) is None
|
||||
|
||||
def test_get_zone_info_with_valid_timezone_string(self):
|
||||
"""Test with a valid timezone string."""
|
||||
tz = "Asia/Tokyo"
|
||||
result = get_zone_info(tz)
|
||||
assert isinstance(result, zoneinfo.ZoneInfo)
|
||||
assert str(result) == "Asia/Tokyo"
|
||||
|
||||
def test_get_zone_info_with_invalid_timezone_string(self):
|
||||
"""Test with an invalid timezone string."""
|
||||
with pytest.raises(
|
||||
zoneinfo.ZoneInfoNotFoundError,
|
||||
match=r"No time zone found.*Invalid/Timezone.*install the tzdata",
|
||||
):
|
||||
get_zone_info("Invalid/Timezone")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("arg", "timedelta_result", "number_result"),
|
||||
[
|
||||
|
||||
@@ -75,7 +75,7 @@ class TestFiles:
|
||||
telegram._utils.files.parse_file_input(string, local_mode=False), InputFile
|
||||
)
|
||||
elif expected_non_local is ValueError:
|
||||
with pytest.raises(ValueError, match="but local mode is not enabled."):
|
||||
with pytest.raises(ValueError, match="but local mode is not enabled\\."):
|
||||
telegram._utils.files.parse_file_input(string, local_mode=False)
|
||||
else:
|
||||
assert (
|
||||
|
||||
@@ -2359,6 +2359,58 @@ class TestApplication:
|
||||
thread.join(timeout=10)
|
||||
assert not thread.is_alive(), "Test took to long to run. Aborting"
|
||||
|
||||
@pytest.mark.parametrize("method_name", ["run_polling", "run_webhook"])
|
||||
async def test_run_polling_webhook_infinite_bootstrap_retries(
|
||||
self, monkeypatch, offline_bot, method_name
|
||||
):
|
||||
"""Here we simply test that setting `bootstrap_retries=-1` does not lead to the wrong
|
||||
infinite-loop behavior reported in #4966. Raising an exception on the first call to
|
||||
`initialize` ensures that a retry actually happens.
|
||||
"""
|
||||
|
||||
def thread_target():
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
|
||||
async def post_init(application):
|
||||
application.stop_running()
|
||||
|
||||
app = (
|
||||
ApplicationBuilder()
|
||||
.bot(offline_bot)
|
||||
.application_class(PytestApplication)
|
||||
.post_init(post_init)
|
||||
.build()
|
||||
)
|
||||
|
||||
async def do_pass(*args, **kwargs):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(app.bot, "initialize", do_pass)
|
||||
monkeypatch.setattr(app.bot, "delete_webhook", do_pass)
|
||||
|
||||
original_initialize = app.initialize
|
||||
|
||||
async def initialize(*args, **kwargs):
|
||||
if self.count >= 3:
|
||||
pytest.fail("Should be called only once. Test failed.")
|
||||
|
||||
self.count += 1
|
||||
if self.count == 1:
|
||||
raise TelegramError("Test Exception")
|
||||
await original_initialize(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(app, "initialize", initialize)
|
||||
getattr(app, method_name)(
|
||||
bootstrap_retries=-1,
|
||||
close_loop=False,
|
||||
stop_signals=None,
|
||||
)
|
||||
|
||||
thread = Thread(target=thread_target)
|
||||
thread.start()
|
||||
thread.join(timeout=10)
|
||||
assert not thread.is_alive(), "Test took to long to run. Aborting"
|
||||
|
||||
def test_signal_handlers(self, offline_bot, monkeypatch):
|
||||
# this test should make sure that signal handlers are set by default on Linux + Mac,
|
||||
# and not on Windows.
|
||||
|
||||
@@ -110,7 +110,7 @@ class TestApplicationBuilder:
|
||||
ApplicationBuilder()
|
||||
|
||||
def test_build_without_token(self, builder):
|
||||
with pytest.raises(RuntimeError, match="No bot token was set."):
|
||||
with pytest.raises(RuntimeError, match="No bot token was set\\."):
|
||||
builder.build()
|
||||
|
||||
def test_build_custom_bot(self, builder, bot):
|
||||
|
||||
@@ -404,7 +404,7 @@ class TestBasePersistence:
|
||||
|
||||
@default_papp
|
||||
def test_set_bot_error(self, papp):
|
||||
with pytest.raises(TypeError, match="when using telegram.ext.ExtBot"):
|
||||
with pytest.raises(TypeError, match="when using telegram\\.ext\\.ExtBot"):
|
||||
papp.persistence.set_bot(Bot(papp.bot.token))
|
||||
|
||||
# just making sure that setting an ExtBoxt without callback_data_cache doesn't raise an
|
||||
@@ -419,7 +419,7 @@ class TestBasePersistence:
|
||||
self.store_data = PersistenceInput(False, False, False, False)
|
||||
|
||||
with pytest.raises(
|
||||
TypeError, match="persistence must be based on telegram.ext.BasePersistence"
|
||||
TypeError, match="persistence must be based on telegram\\.ext\\.BasePersistence"
|
||||
):
|
||||
ApplicationBuilder().bot(bot).persistence(MyPersistence()).build()
|
||||
|
||||
|
||||
@@ -200,12 +200,12 @@ class TestCallbackContext:
|
||||
|
||||
callback_context = CallbackContext.from_update(update, app)
|
||||
|
||||
with pytest.raises(RuntimeError, match="This telegram.ext.ExtBot instance does not"):
|
||||
with pytest.raises(RuntimeError, match="This telegram\\.ext\\.ExtBot instance does not"):
|
||||
callback_context.drop_callback_data(None)
|
||||
|
||||
try:
|
||||
app.bot = raw_bot
|
||||
with pytest.raises(RuntimeError, match="telegram.Bot does not allow for"):
|
||||
with pytest.raises(RuntimeError, match="telegram\\.Bot does not allow for"):
|
||||
callback_context.drop_callback_data(None)
|
||||
finally:
|
||||
app.bot = bot
|
||||
|
||||
@@ -320,7 +320,7 @@ class TestCallbackDataCache:
|
||||
data=out.inline_keyboard[0][1].callback_data,
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError, match="CallbackQuery was not found in cache."):
|
||||
with pytest.raises(KeyError, match="CallbackQuery was not found in cache\\."):
|
||||
callback_data_cache.drop_data(callback_query)
|
||||
|
||||
callback_data_cache.process_callback_query(callback_query)
|
||||
|
||||
@@ -629,7 +629,7 @@ class TestJobQueue:
|
||||
async def test_attribute_error(self):
|
||||
job = Job(self.job_run_once)
|
||||
with pytest.raises(
|
||||
AttributeError, match="nor 'apscheduler.job.Job' has attribute 'error'"
|
||||
AttributeError, match="nor 'apscheduler\\.job\\.Job' has attribute 'error'"
|
||||
):
|
||||
job.error
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ class TestMessageReactionHandler:
|
||||
)
|
||||
async def test_username_with_anonymous_reaction(self, app, allowed_types, kwargs):
|
||||
with pytest.raises(
|
||||
ValueError, match="You can not filter for users and include anonymous reactions."
|
||||
ValueError, match="You can not filter for users and include anonymous reactions\\."
|
||||
):
|
||||
MessageReactionHandler(self.callback, message_reaction_types=allowed_types, **kwargs)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestBaseRateLimiter:
|
||||
request_received = None
|
||||
|
||||
async def test_no_rate_limiter(self, bot):
|
||||
with pytest.raises(ValueError, match="if a `ExtBot.rate_limiter` is set"):
|
||||
with pytest.raises(ValueError, match="if a `ExtBot\\.rate_limiter` is set"):
|
||||
await bot.send_message(chat_id=42, text="test", rate_limit_args="something")
|
||||
|
||||
async def test_argument_passing(self, bot_info, monkeypatch, bot):
|
||||
|
||||
@@ -849,7 +849,7 @@ class TestUpdater:
|
||||
)
|
||||
async def test_no_unix(self, updater):
|
||||
async with updater:
|
||||
with pytest.raises(RuntimeError, match="binding unix sockets."):
|
||||
with pytest.raises(RuntimeError, match="binding unix sockets\\."):
|
||||
await updater.start_webhook(unix="DoesntMatter", webhook_url="TOKEN")
|
||||
|
||||
async def test_start_webhook_already_running(self, updater, monkeypatch):
|
||||
@@ -1141,3 +1141,52 @@ class TestUpdater:
|
||||
|
||||
await updater.stop()
|
||||
assert not updater.running
|
||||
|
||||
@pytest.mark.parametrize("method_name", ["start_polling", "start_webhook"])
|
||||
async def test_infinite_bootstrap_retries(self, updater, monkeypatch, method_name):
|
||||
"""Here we simply test that setting `bootstrap_retries=-1` does not lead to the wrong
|
||||
infinite-loop behavior reported in #4966. Raising an exception on the first call to
|
||||
`set/delete_webhook` ensures that a retry actually happens.
|
||||
"""
|
||||
|
||||
original_delete_webhook = updater.bot.delete_webhook
|
||||
original_set_webhook = updater.bot.set_webhook
|
||||
counts = {"delete": 0, "set": 0}
|
||||
|
||||
def patch_builder(func, name):
|
||||
async def wrapped(*args, **kwargs):
|
||||
if counts[name] >= 3:
|
||||
pytest.fail("Should be called only once. Test failed.")
|
||||
counts[name] += 1
|
||||
if counts[name] == 1:
|
||||
raise TelegramError("1")
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
async def get_updates(*args, **kwargs):
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(
|
||||
updater.bot, "delete_webhook", patch_builder(original_delete_webhook, "delete")
|
||||
)
|
||||
monkeypatch.setattr(updater.bot, "set_webhook", patch_builder(original_set_webhook, "set"))
|
||||
monkeypatch.setattr(updater.bot, "get_updates", get_updates)
|
||||
|
||||
kwargs = {"bootstrap_retries": -1}
|
||||
if method_name == "start_webhook":
|
||||
kwargs.update(
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": randrange(1024, 49152),
|
||||
}
|
||||
)
|
||||
|
||||
async with updater:
|
||||
task = asyncio.create_task(getattr(updater, method_name)(**kwargs))
|
||||
try:
|
||||
await asyncio.wait_for(task, timeout=10)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"{method_name} did not succeed within the timeout. Aborting.")
|
||||
finally:
|
||||
await updater.stop()
|
||||
|
||||
@@ -273,7 +273,7 @@ class TestRequestWithoutRequest:
|
||||
|
||||
with pytest.raises(
|
||||
BadRequest,
|
||||
match="{'unknown': '42'}",
|
||||
match="\\{'unknown': '42'\\}",
|
||||
):
|
||||
await httpx_request.post(None, None, None)
|
||||
|
||||
|
||||
+2
-2
@@ -2672,7 +2672,7 @@ class TestBotWithRequest:
|
||||
# No need to duplicate here.
|
||||
|
||||
async def test_invalid_token_server_response(self):
|
||||
with pytest.raises(InvalidToken, match="The token `12` was rejected by the server."):
|
||||
with pytest.raises(InvalidToken, match="The token `12` was rejected by the server\\."):
|
||||
async with ExtBot(token="12"):
|
||||
pass
|
||||
|
||||
@@ -3835,7 +3835,7 @@ class TestBotWithRequest:
|
||||
#
|
||||
# The error message Hide_requester_missing started showing up instead of
|
||||
# User_already_participant. Don't know why …
|
||||
with pytest.raises(BadRequest, match="User_already_participant|Hide_requester_missing"):
|
||||
with pytest.raises(BadRequest, match=r"User_already_participant|Hide_requester_missing"):
|
||||
await bot.decline_chat_join_request(chat_id=channel_id, user_id=chat_id)
|
||||
|
||||
async def test_set_chat_photo(self, bot, channel_id):
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import datetime as dtm
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -554,3 +555,227 @@ class TestBusinessOpeningHoursWithoutRequest(BusinessTestBase):
|
||||
|
||||
assert boh1 != boh3
|
||||
assert hash(boh1) != hash(boh3)
|
||||
|
||||
class TestBusinessOpeningHoursGetOpeningHoursForDayWithoutRequest:
|
||||
@pytest.fixture
|
||||
def sample_opening_hours(self):
|
||||
# Monday 8am-8:30pm (480-1230)
|
||||
# Tuesday 24 hours (1440-2879)
|
||||
# Sunday 12am-11:58pm (8640-10078)
|
||||
intervals = [
|
||||
BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm
|
||||
BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours
|
||||
BusinessOpeningHoursInterval(8640, 10078), # Sunday 12am-11:58pm
|
||||
]
|
||||
return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
|
||||
|
||||
def test_monday_opening_hours(self, sample_opening_hours):
|
||||
# Test for Monday
|
||||
test_date = dtm.date(2023, 11, 6) # Monday
|
||||
time_zone = ZoneInfo("UTC")
|
||||
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
|
||||
|
||||
expected = (
|
||||
(
|
||||
dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone),
|
||||
dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone),
|
||||
),
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
|
||||
def test_tuesday_24_hours(self, sample_opening_hours):
|
||||
# Test for Tuesday (24 hours)
|
||||
test_date = dtm.date(2023, 11, 7) # Tuesday
|
||||
time_zone = ZoneInfo("UTC")
|
||||
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
|
||||
|
||||
expected = (
|
||||
(
|
||||
dtm.datetime(2023, 11, 7, 0, 0, tzinfo=time_zone),
|
||||
dtm.datetime(2023, 11, 7, 23, 59, tzinfo=time_zone),
|
||||
),
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
|
||||
def test_sunday_opening_hours(self, sample_opening_hours):
|
||||
# Test for Sunday
|
||||
test_date = dtm.date(2023, 11, 12) # Sunday
|
||||
time_zone = ZoneInfo("UTC")
|
||||
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
|
||||
|
||||
expected = (
|
||||
(
|
||||
dtm.datetime(2023, 11, 12, 0, 0, tzinfo=time_zone),
|
||||
dtm.datetime(2023, 11, 12, 23, 58, tzinfo=time_zone),
|
||||
),
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
|
||||
def test_day_with_no_opening_hours(self, sample_opening_hours):
|
||||
# Test for Wednesday (no opening hours defined)
|
||||
test_date = dtm.date(2023, 11, 8) # Wednesday
|
||||
time_zone = ZoneInfo("UTC")
|
||||
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
|
||||
|
||||
assert result == ()
|
||||
|
||||
def test_multiple_intervals_same_day(self):
|
||||
# Test with multiple intervals on the same day
|
||||
intervals = [
|
||||
# unsorted on purpose to check that the sorting works (even though this is
|
||||
# currently undocumented behaviour)
|
||||
BusinessOpeningHoursInterval(900, 1230), # Monday 3pm-8:30pm
|
||||
BusinessOpeningHoursInterval(480, 720), # Monday 8am-12pm
|
||||
]
|
||||
opening_hours = BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
|
||||
|
||||
test_date = dtm.date(2023, 11, 6) # Monday
|
||||
time_zone = ZoneInfo("UTC")
|
||||
result = opening_hours.get_opening_hours_for_day(test_date, time_zone)
|
||||
|
||||
expected = (
|
||||
(
|
||||
dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone),
|
||||
dtm.datetime(2023, 11, 6, 12, 0, tzinfo=time_zone),
|
||||
),
|
||||
(
|
||||
dtm.datetime(2023, 11, 6, 15, 0, tzinfo=time_zone),
|
||||
dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone),
|
||||
),
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
|
||||
@pytest.mark.parametrize("input_type", [str, ZoneInfo])
|
||||
def test_timezone_conversion(self, sample_opening_hours, input_type):
|
||||
# Test that timezone is properly applied
|
||||
test_date = dtm.date(2023, 11, 6) # Monday
|
||||
time_zone = input_type("America/New_York")
|
||||
zone_info = ZoneInfo("America/New_York")
|
||||
result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone)
|
||||
|
||||
expected = (
|
||||
(
|
||||
dtm.datetime(2023, 11, 6, 3, 0, tzinfo=zone_info),
|
||||
dtm.datetime(2023, 11, 6, 15, 30, tzinfo=zone_info),
|
||||
),
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
assert result[0][0].tzinfo == zone_info
|
||||
assert result[0][1].tzinfo == zone_info
|
||||
|
||||
def test_timezone_conversation_changing_date(self):
|
||||
# test for the edge case where the returned time is on a different date in the target
|
||||
# timezone than in the business timezone
|
||||
intervals = [
|
||||
BusinessOpeningHoursInterval(60, 120), # Monday 1am-2am UTC
|
||||
]
|
||||
opening_hours = BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
|
||||
test_date = dtm.date(2023, 11, 6) # Monday
|
||||
time_zone = ZoneInfo("America/New_York") # UTC-5, so 1am UTC is 8pm previous day
|
||||
result = opening_hours.get_opening_hours_for_day(test_date, time_zone)
|
||||
expected = (
|
||||
(
|
||||
dtm.datetime(2023, 11, 5, 20, 0, tzinfo=time_zone),
|
||||
dtm.datetime(2023, 11, 5, 21, 0, tzinfo=time_zone),
|
||||
),
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
def test_no_timezone_provided(self, sample_opening_hours):
|
||||
# Test when no timezone is provided
|
||||
test_date = dtm.date(2023, 11, 6) # Monday
|
||||
result = sample_opening_hours.get_opening_hours_for_day(test_date)
|
||||
|
||||
expected = (
|
||||
(
|
||||
dtm.datetime(
|
||||
2023,
|
||||
11,
|
||||
6,
|
||||
8,
|
||||
0,
|
||||
tzinfo=ZoneInfo(sample_opening_hours.time_zone_name),
|
||||
),
|
||||
dtm.datetime(
|
||||
2023,
|
||||
11,
|
||||
6,
|
||||
20,
|
||||
30,
|
||||
tzinfo=ZoneInfo(sample_opening_hours.time_zone_name),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
|
||||
class TestBusinessOpeningHoursIsOpenWithoutRequest:
|
||||
@pytest.fixture
|
||||
def sample_opening_hours(self):
|
||||
# Monday 8am-8:30pm (480-1230)
|
||||
# Tuesday 24 hours (1440-2879)
|
||||
# Sunday 12am-11:59pm (8640-10079)
|
||||
intervals = [
|
||||
BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm UTC
|
||||
BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours UTC
|
||||
BusinessOpeningHoursInterval(8640, 10079), # Sunday 12am-11:59pm UTC
|
||||
]
|
||||
return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals)
|
||||
|
||||
def test_is_open_during_business_hours(self, sample_opening_hours):
|
||||
# Monday 10am UTC (within 8am-8:30pm)
|
||||
dt = dtm.datetime(2023, 11, 6, 10, 0, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is True
|
||||
|
||||
def test_is_open_at_opening_time(self, sample_opening_hours):
|
||||
# Monday exactly 8am UTC
|
||||
dt = dtm.datetime(2023, 11, 6, 8, 0, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is True
|
||||
|
||||
def test_is_closed_at_closing_time(self, sample_opening_hours):
|
||||
# Monday exactly 8:30pm UTC (closing time is exclusive)
|
||||
dt = dtm.datetime(2023, 11, 6, 20, 30, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is False
|
||||
|
||||
def test_is_closed_outside_business_hours(self, sample_opening_hours):
|
||||
# Monday 7am UTC (before opening)
|
||||
dt = dtm.datetime(2023, 11, 6, 7, 0, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is False
|
||||
|
||||
def test_is_open_24h_day(self, sample_opening_hours):
|
||||
# Tuesday 3am UTC (24h opening)
|
||||
dt = dtm.datetime(2023, 11, 7, 3, 0, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is True
|
||||
|
||||
def test_is_closed_on_day_with_no_hours(self, sample_opening_hours):
|
||||
# Wednesday (no opening hours)
|
||||
dt = dtm.datetime(2023, 11, 8, 12, 0, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is False
|
||||
|
||||
def test_timezone_conversion(self, sample_opening_hours):
|
||||
# Monday 5am EDT is 10am UTC (should be open)
|
||||
dt = dtm.datetime(2023, 11, 6, 5, 0, tzinfo=ZoneInfo("America/New_York"))
|
||||
assert sample_opening_hours.is_open(dt) is True
|
||||
|
||||
# Monday 2am EDT is 7am UTC (should be closed)
|
||||
dt = dtm.datetime(2023, 11, 6, 2, 0, tzinfo=ZoneInfo("America/New_York"))
|
||||
assert sample_opening_hours.is_open(dt) is False
|
||||
|
||||
def test_naive_datetime_uses_business_timezone(self, sample_opening_hours):
|
||||
# Naive datetime - should be interpreted as UTC (business timezone)
|
||||
dt = dtm.datetime(2023, 11, 6, 10, 0) # 10am naive
|
||||
assert sample_opening_hours.is_open(dt) is True
|
||||
|
||||
def test_boundary_conditions(self, sample_opening_hours):
|
||||
# Sunday 11:58pm UTC (should be open)
|
||||
dt = dtm.datetime(2023, 11, 12, 23, 58, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is True
|
||||
|
||||
# Sunday 11:59pm UTC (should be closed)
|
||||
dt = dtm.datetime(2023, 11, 12, 23, 59, tzinfo=ZoneInfo("UTC"))
|
||||
assert sample_opening_hours.is_open(dt) is False
|
||||
|
||||
+17
-17
@@ -42,23 +42,23 @@ from tests.auxil.slots import mro_slots
|
||||
|
||||
class TestErrors:
|
||||
def test_telegram_error(self):
|
||||
with pytest.raises(TelegramError, match="^test message$"):
|
||||
with pytest.raises(TelegramError, match=r"^test message$"):
|
||||
raise TelegramError("test message")
|
||||
with pytest.raises(TelegramError, match="^Test message$"):
|
||||
with pytest.raises(TelegramError, match=r"^Test message$"):
|
||||
raise TelegramError("Error: test message")
|
||||
with pytest.raises(TelegramError, match="^Test message$"):
|
||||
with pytest.raises(TelegramError, match=r"^Test message$"):
|
||||
raise TelegramError("[Error]: test message")
|
||||
with pytest.raises(TelegramError, match="^Test message$"):
|
||||
with pytest.raises(TelegramError, match=r"^Test message$"):
|
||||
raise TelegramError("Bad Request: test message")
|
||||
|
||||
def test_unauthorized(self):
|
||||
with pytest.raises(Forbidden, match="test message"):
|
||||
raise Forbidden("test message")
|
||||
with pytest.raises(Forbidden, match="^Test message$"):
|
||||
with pytest.raises(Forbidden, match=r"^Test message$"):
|
||||
raise Forbidden("Error: test message")
|
||||
with pytest.raises(Forbidden, match="^Test message$"):
|
||||
with pytest.raises(Forbidden, match=r"^Test message$"):
|
||||
raise Forbidden("[Error]: test message")
|
||||
with pytest.raises(Forbidden, match="^Test message$"):
|
||||
with pytest.raises(Forbidden, match=r"^Test message$"):
|
||||
raise Forbidden("Bad Request: test message")
|
||||
|
||||
def test_invalid_token(self):
|
||||
@@ -68,25 +68,25 @@ class TestErrors:
|
||||
def test_network_error(self):
|
||||
with pytest.raises(NetworkError, match="test message"):
|
||||
raise NetworkError("test message")
|
||||
with pytest.raises(NetworkError, match="^Test message$"):
|
||||
with pytest.raises(NetworkError, match=r"^Test message$"):
|
||||
raise NetworkError("Error: test message")
|
||||
with pytest.raises(NetworkError, match="^Test message$"):
|
||||
with pytest.raises(NetworkError, match=r"^Test message$"):
|
||||
raise NetworkError("[Error]: test message")
|
||||
with pytest.raises(NetworkError, match="^Test message$"):
|
||||
with pytest.raises(NetworkError, match=r"^Test message$"):
|
||||
raise NetworkError("Bad Request: test message")
|
||||
|
||||
def test_bad_request(self):
|
||||
with pytest.raises(BadRequest, match="test message"):
|
||||
raise BadRequest("test message")
|
||||
with pytest.raises(BadRequest, match="^Test message$"):
|
||||
with pytest.raises(BadRequest, match=r"^Test message$"):
|
||||
raise BadRequest("Error: test message")
|
||||
with pytest.raises(BadRequest, match="^Test message$"):
|
||||
with pytest.raises(BadRequest, match=r"^Test message$"):
|
||||
raise BadRequest("[Error]: test message")
|
||||
with pytest.raises(BadRequest, match="^Test message$"):
|
||||
with pytest.raises(BadRequest, match=r"^Test message$"):
|
||||
raise BadRequest("Bad Request: test message")
|
||||
|
||||
def test_timed_out(self):
|
||||
with pytest.raises(TimedOut, match="^Timed out$"):
|
||||
with pytest.raises(TimedOut, match=r"^Timed out$"):
|
||||
raise TimedOut
|
||||
|
||||
def test_chat_migrated(self):
|
||||
@@ -97,11 +97,11 @@ class TestErrors:
|
||||
@pytest.mark.parametrize("retry_after", [12, dtm.timedelta(seconds=12)])
|
||||
def test_retry_after(self, PTB_TIMEDELTA, retry_after):
|
||||
if PTB_TIMEDELTA:
|
||||
with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 0:00:12"):
|
||||
with pytest.raises(RetryAfter, match="Flood control exceeded\\. Retry in 0:00:12"):
|
||||
raise (exception := RetryAfter(retry_after))
|
||||
assert type(exception.retry_after) is dtm.timedelta
|
||||
else:
|
||||
with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 12 seconds"):
|
||||
with pytest.raises(RetryAfter, match="Flood control exceeded\\. Retry in 12 seconds"):
|
||||
raise (exception := RetryAfter(retry_after))
|
||||
assert type(exception.retry_after) is int
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestErrors:
|
||||
assert type(retry_after) is int
|
||||
|
||||
def test_conflict(self):
|
||||
with pytest.raises(Conflict, match="Something something."):
|
||||
with pytest.raises(Conflict, match="Something something\\."):
|
||||
raise Conflict("Something something.")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -603,13 +603,13 @@ class TestMessageWithoutRequest(MessageTestBase):
|
||||
"""Used in testing reply_* below. Makes sure that do_quote is handled correctly"""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="`reply_to_message_id` and `reply_parameters` are mutually exclusive.",
|
||||
match="`reply_to_message_id` and `reply_parameters` are mutually exclusive\\.",
|
||||
):
|
||||
await method(*args, reply_to_message_id=42, reply_parameters=42)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="`allow_sending_without_reply` and `reply_parameters` are mutually exclusive.",
|
||||
match="`allow_sending_without_reply` and `reply_parameters` are mutually exclusive\\.",
|
||||
):
|
||||
await method(*args, allow_sending_without_reply=True, reply_parameters=42)
|
||||
|
||||
@@ -1463,7 +1463,7 @@ class TestMessageWithoutRequest(MessageTestBase):
|
||||
message.text = "AA"
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="You requested the 5-th occurrence of 'A', but this text appears only 2 times.",
|
||||
match="You requested the 5-th occurrence of 'A', but this text appears only 2 times",
|
||||
):
|
||||
message.compute_quote_position_and_entities("A", 5)
|
||||
|
||||
@@ -1472,7 +1472,7 @@ class TestMessageWithoutRequest(MessageTestBase):
|
||||
message.caption = None
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="This message has neither text nor caption.",
|
||||
match="This message has neither text nor caption\\.",
|
||||
):
|
||||
message.compute_quote_position_and_entities("A", 5)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user