Compare commits

...

30 Commits

Author SHA1 Message Date
Noam Meltzer 1e7f4fae6f Bump version to v12.3.0 2020-01-11 21:21:46 +02:00
Noam Meltzer dae5ab47a0 CHANGES.rst: Update towards v12.3.0 release 2020-01-11 21:08:03 +02:00
Bibo-Joshi a9d9b1d750 Add #1653 to changelog 2020-01-11 20:57:58 +02:00
Hinrich Mahler 90496f70a5 Prepare for v12.3.0 2020-01-11 20:57:58 +02:00
tobiaswicker 940b42e048 ConversationHandler: Fix wrong signature call for timeout handlers
Fixes #1652

Co-authored-by: Eldinnie <Eldinnie@users.noreply.github.com>
2020-01-11 20:51:31 +02:00
Poolitzer a582515766 README.rst: replace travis and appveyor with github (#1645) 2020-01-11 20:47:21 +02:00
Eldinnie 3d42df3366 Fix documentation about return values in message (#1656)
* Fix documentation about return values in message

* flake8 satisfaction
2019-12-16 14:22:11 +01:00
Poolitzer 2c67a9833b add private /c links to message.links object (#1619)
* add private /c links to message.links object

* fixing ids for basic groups

* fixing ids for non basic chats and the test

* Improve tests for Message.link

* Simplify id_to_link
2019-11-29 13:50:44 +01:00
Bibo-Joshi 5e8a961669 Refactor msg_in (#1631)
* Revert "Add msg_in filter (new) (#1570)"

This reverts commit 34bdbc632a.

* Refactor msg_in, add Filters.caption on the fly

* Update docstrings

* Fix copy-paste typo
2019-11-29 13:09:44 +01:00
Bibo-Joshi a5ba64becb Remove checks for None on assignement for opt args (#1600) 2019-11-23 18:05:03 +02:00
Noam Meltzer 2a3169a22f Fix deprecation warning with Python 3.8 triggered by vendored urllib3 (#1618)
Fixes #1586
2019-11-16 14:37:15 +02:00
Poolitzer 894d8281ab github workflow: add a cron job (#1615) 2019-11-16 00:19:47 +02:00
Noam Meltzer 2fdf48023b github workflow: give pre-commit its own job (#1612) 2019-11-15 23:35:44 +02:00
Paolo Lammens 4e717a172b Fix UTC/local inconsistencies for naive datetimes (#1506) 2019-11-15 22:51:22 +02:00
Noam Meltzer 10c9ec2313 workflow: run test-official in its own job (#1607) 2019-11-09 22:38:23 +02:00
Noam Meltzer 096a7c3593 Allow PRs to test (#1606) 2019-11-09 22:13:02 +02:00
Noam Meltzer e9d9f01bd4 Fix windows actions (#1605)
* Stop testing with ujson

* Fix timing issue with test_delete_message

* ignore pip deprecation warning. hopefully it will fix windows+py2.7

* telegram replies to deleteing old messages are not consistent
2019-11-09 20:33:51 +02:00
Jasmin Bom 8b4b22cc89 Implement Github Actions CI (#1556)
* Add test workflow

* Attempt github grouping

* Improve bot info fetching

- Add support for b64+json encoded github secret with all the vars
- Add bot_name and bot_username since it's needed for a proper get_me test

* Improve test workflow a lot

- Add coverage
- Install ujson
- test_official only run on in single job
- Pass bot info to pytest

* Improve github grouping by having shorter titles

* Run pytest with coverage

* Improve coverage report

* Proper exitcode behaviour for pytest

* Proper test official handling

* Proper error handling

* Skip jobqueue tests on windows

* run coverage tests even if nocoverage ones fail

* Skip messagequeue tests on windows

* Clean up to satisfy flake8

* Run meta tests
2019-10-27 14:28:33 +01:00
Bibo-Joshi b294c92bad question template: Add info about users' group (#1577) 2019-10-27 14:33:30 +02:00
Poolitzer 264de2b7c1 Github actions - notify maintainers about changed examples (#1555) 2019-10-27 01:42:47 +03:00
Julian Ste ac64027580 Fixed comments in examples (#1566) 2019-10-27 01:15:09 +03:00
Bibo-Joshi 34bdbc632a Add msg_in filter (new) (#1570)
Closes #1144
2019-10-27 01:12:54 +03:00
Bibo-Joshi bbcff96804 Doc fixes (#1572)
Fixes #1576
2019-10-27 01:04:48 +03:00
Bibo-Joshi 93449443b2 Add *args, **kwargs to Message.forward() (#1574) 2019-10-27 01:00:27 +03:00
Poolitzer 8cdb20a85a updating example to V12 (#1579) 2019-10-27 00:51:29 +03:00
Jannes Höke 6fddb49af5 📝 Update section "Getting help" 2019-10-22 00:04:31 +02:00
Jannes Höke b0aef0c718 🔀 Update issue templates (#1569)
* 📝 Update issue templates

* 👌 Update question template

* 🔥 Delete old issue template
2019-10-21 23:51:58 +02:00
Iulian Onofrei 88eccc6608 Add MAX_PHOTOSIZE_UPLOAD size limit constant (#1560)
* Add MAX_PHOTOSIZE_UPLOAD size limit constant

* Add the other source of the constants' values
2019-10-17 22:24:44 +02:00
Lorenzo Rossi 3d8771bbdf 🔀 Add mutex protection on ConversationHandler (#1533)
* Add mutex protection on ConversationHandler

* Remove timeout job before child update

* Make locks private

* Add conversation timeout conflict test
2019-10-17 00:03:53 +02:00
Iulian Onofrei 7152b5aaf9 Fix CONTRIBUTING.rst not to install requirements as root (#1558) 2019-10-16 21:50:38 +03:00
54 changed files with 1192 additions and 459 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ Setting things up
.. code-block:: bash
$ sudo pip install -r requirements.txt -r requirements-dev.txt
$ pip install -r requirements.txt -r requirements-dev.txt
5. Install pre-commit hooks:
@@ -1,9 +1,16 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: 'bug :bug:'
assignees: ''
---
<!--
Thanks for reporting issues of python-telegram-bot!
Use this template to notify us if you found a bug, or if you want to request a new feature.
If you're looking for help with programming your bot using our library, feel free to ask your
questions in out telegram group at: https://t.me/pythontelegrambotgroup
Use this template to notify us if you found a bug.
To make it easier for us to help you please enter detailed information below.
+24
View File
@@ -0,0 +1,24 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is.
Ex. *I want to do X, but there is no way to do it.*
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
Ex. *I think it would be nice if you would add feature Y so it will make it easier.*
#### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Ex. *I considered Z, but that didn't work because...*
#### Additional context
Add any other context or screenshots about the feature request here.
Ex. *Here's a photo of my cat!*
+29
View File
@@ -0,0 +1,29 @@
---
name: Question
about: Get help with errors or general questions
title: "[QUESTION]"
labels: 'question :question:'
assignees: ''
---
<!--
Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already.
The following template is a suggestion how you can report an issue you run into whilst using our library. If you just want to ask a question, feel free to delete everything; just make sure you have a describing title :)
Please mind that there is also a users' Telegram group at https://t.me/pythontelegrambotgroup for questions about the library. Questions asked there might be answered quicker than here. In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel at https://webchat.freenode.net/?channels=##python-telegram-bot to participate in the group.
-->
### Issue I am facing
Please describe the issue here in as much detail as possible
### Traceback to the issue
```
put it here
```
### Related part of your code
```python
put it here
```
+14
View File
@@ -0,0 +1,14 @@
name: Warning maintainers
on:
pull_request:
paths: examples/**
jobs:
job:
runs-on: ubuntu-latest
name: about example change
steps:
- name: running the check
uses: Poolitzer/notifier-action@master
with:
notify-message: Hey there. Relax, I am just a little warning for the maintainers to release directly after merging your PR, otherwise we have broken examples and people might get confused :)
repo-token: ${{ secrets.GITHUB_TOKEN }}
+116
View File
@@ -0,0 +1,116 @@
name: Testing your PR
on:
pull_request:
branches:
- master
schedule:
- cron: 7 3 * * *
jobs:
pytest:
name: pytest
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7]
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
python-version: 3.7
test-build: True
- os: windows-latest
python-version: 3.7
test-build: True
fail-fast: False
steps:
- uses: actions/checkout@v1
- name: Initialize vendored libs
run:
git submodule update --init --recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -U codecov pytest-cov
python -W ignore -m pip install -r requirements.txt
python -W ignore -m pip install -r requirements-dev.txt
- name: Test with pytest
run: |
pytest -v -m nocoverage
nocov_exit=$?
pytest -v -m "not nocoverage" --cov
cov_exit=$?
global_exit=$(( nocov_exit > cov_exit ? nocov_exit : cov_exit ))
exit ${global_exit}
env:
JOB_INDEX: ${{ strategy.job-index }}
BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifV0=
TEST_BUILD: ${{ matrix.test-build }}
TEST_PRE_COMMIT: ${{ matrix.test-pre-commit }}
shell: bash --noprofile --norc {0}
- name: Submit coverage
run: |
if [ "$CODECOV_TOKEN" != "" ]; then
codecov -F github -t $CODECOV_TOKEN --name "${{ matrix.os }}-${{ matrix.python-version }}"
fi
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
shell: bash
test_official:
name: test-official
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: [3.7]
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v1
- name: Initialize vendored libs
run:
git submodule update --init --recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements.txt
python -W ignore -m pip install -r requirements-dev.txt
- name: Compare to official api
run: |
pytest -v tests/test_official.py
exit $?
env:
TEST_OFFICIAL: "true"
shell: bash --noprofile --norc {0}
test_pre_commit:
name: test-pre-commit
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: [3.7]
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v1
- name: Initialize vendored libs
run:
git submodule update --init --recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements.txt
python -W ignore -m pip install -r requirements-dev.txt
- name: Run pre-commit tests
run: pre-commit run --all-files
+51
View File
@@ -2,8 +2,59 @@
Changelog
=========
Version 12.3.0
==============
*Released 2020-01-11*
**New features:**
- `Filters.caption` allows only messages with caption (`#1631`_).
- Filter for exact messages/captions with new capability of `Filters.text` and `Filters.caption`. Especially useful in combination with ReplyKeyboardMarkup. (`#1631`_).
**Major changes:**
- Fix inconsistent handling of naive datetimes (`#1506`_).
**Minor changes, CI improvments or bug fixes:**
- Documentation fixes (`#1558`_, `#1569`_, `#1579`_, `#1572`_, `#1566`_, `#1577`_, `#1656`_).
- Add mutex protection on `ConversationHandler` (`#1533`_).
- Add `MAX_PHOTOSIZE_UPLOAD` constant (`#1560`_).
- Add args and kwargs to `Message.forward()` (`#1574`_).
- Transfer to GitHub Actions CI (`#1555`_, `#1556`_, `#1605`_, `#1606`_, `#1607`_, `#1612`_, `#1615`_, `#1645`_).
- Fix deprecation warning with Py3.8 by vendored urllib3 (`#1618`_).
- Simplify assignements for optional arguments (`#1600`_)
- Allow private groups for `Message.link` (`#1619`_).
- Fix wrong signature call for `ConversationHandler.TIMEOUT` handlers (`#1653`_).
.. _`#1631`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1558
.. _`#1506`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1506
.. _`#1558`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1558
.. _`#1569`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1569
.. _`#1579`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1579
.. _`#1572`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1572
.. _`#1566`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1566
.. _`#1577`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1577
.. _`#1533`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1533
.. _`#1560`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1560
.. _`#1574`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1574
.. _`#1555`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1555
.. _`#1556`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1556
.. _`#1605`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1605
.. _`#1606`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1606
.. _`#1607`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1607
.. _`#1612`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1612
.. _`#1615`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1615
.. _`#1618`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1618
.. _`#1600`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1600
.. _`#1619`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1619
.. _`#1653`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1653
.. _`#1656`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1656
.. _`#1645`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1645
Version 12.2.0
==============
*Released 2019-10-14*
**New features:**
+7 -11
View File
@@ -29,14 +29,9 @@ We have a vibrant community of developers helping each other in our `Telegram gr
:target: https://www.gnu.org/licenses/lgpl-3.0.html
:alt: LGPLv3 License
.. image:: https://travis-ci.org/python-telegram-bot/python-telegram-bot.svg?branch=master
:target: https://travis-ci.org/python-telegram-bot/python-telegram-bot
:alt: Travis CI Status
.. image:: https://img.shields.io/appveyor/ci/python-telegram-bot/python-telegram-bot/master.svg?logo=appveyor
:target: https://ci.appveyor.com/project/python-telegram-bot/python-telegram-bot
:alt: AppVeyor CI Status
.. image:: https://github.com/python-telegram-bot/python-telegram-bot/workflows/Testing%20your%20PR/badge.svg?event=schedule
:target: https://github.com/python-telegram-bot/python-telegram-bot/
:alt: Github Actions workflow
.. image:: https://codecov.io/gh/python-telegram-bot/python-telegram-bot/branch/master/graph/badge.svg
:target: https://codecov.io/gh/python-telegram-bot/python-telegram-bot
@@ -192,11 +187,12 @@ You can get help in several ways:
1. We have a vibrant community of developers helping each other in our `Telegram group <https://telegram.me/pythontelegrambotgroup>`_. Join us!
2. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
2. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_.
3. You can ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
3. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
4. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
4. As last resort, the developers are ready to help you with `serious issues <https://github.com/python-telegram-bot/python-telegram-bot/issues/new>`_.
============
+2 -2
View File
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '12.1' # telegram.__version__[:3]
version = '12.3' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '12.2.0' # telegram.__version__
release = '12.3.0' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+2 -2
View File
@@ -1,6 +1,6 @@
# Examples
In this folder there are small examples to show what a bot written with `python-telegram-bot` looks like. Some bots focus on one specific aspect of the Telegram Bot API while others focus on one of the mechanics of this library. Except for the [`echobot.py`](#pure-api) example, they all use the high-level framework this library provides with the [`telegram.ext`](https://python-telegram-bot.readthedocs.io/en/latest/telegram.ext.html) submodule.
In this folder are small examples to show what a bot written with `python-telegram-bot` looks like. Some bots focus on one specific aspect of the Telegram Bot API while others focus on one of the mechanics of this library. Except for the [`echobot.py`](#pure-api) example, they all use the high-level framework this library provides with the [`telegram.ext`](https://python-telegram-bot.readthedocs.io/en/latest/telegram.ext.html) submodule.
All examples are licensed under the [CC0 License](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/LICENSE.txt) and are therefore fully dedicated to the public domain. You can use them as the base for your own bots without worrying about copyrights.
@@ -35,4 +35,4 @@ A basic example of a bot that can accept payments. Don't forget to enable and co
A basic example of a bot store conversation state and user_data over multiple restarts.
## Pure API
The [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py) example uses only the pure, "bare-metal" API wrapper.
The [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py) example uses only the pure, "bare-metal" API wrapper.
+3 -3
View File
@@ -21,7 +21,7 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__)
def msg(bot, update):
def msg(update, context):
# If we received any passport data
passport_data = update.message.passport_data
if passport_data:
@@ -77,9 +77,9 @@ def msg(bot, update):
actual_file.download()
def error(bot, update, error):
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, error)
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
+2 -2
View File
@@ -42,7 +42,7 @@ def start_with_shipping_callback(update, context):
currency = "USD"
# price in dollars
price = 1
# price * 100 so as to include 2 d.p.
# price * 100 so as to include 2 decimal points
# check https://core.telegram.org/bots/payments#supported-currencies for more details
prices = [LabeledPrice("Test", price * 100)]
@@ -66,7 +66,7 @@ def start_without_shipping_callback(update, context):
currency = "USD"
# price in dollars
price = 1
# price * 100 so as to include 2 d.p.
# price * 100 so as to include 2 decimal points
prices = [LabeledPrice("Test", price * 100)]
# optionally pass need_name=True, need_phone_number=True,
+4 -1
View File
@@ -17,7 +17,8 @@
"""Constants in the Telegram network.
The following constants were extracted from the
`Telegram Bots FAQ <https://core.telegram.org/bots/faq>`_.
`Telegram Bots FAQ <https://core.telegram.org/bots/faq>`_ and
`Telegram Bots API <https://core.telegram.org/bots/api>`_.
Attributes:
MAX_MESSAGE_LENGTH (:obj:`int`): 4096
@@ -25,6 +26,7 @@ Attributes:
SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD (:obj:`int`): In bytes (20MB)
MAX_FILESIZE_UPLOAD (:obj:`int`): In bytes (50MB)
MAX_PHOTOSIZE_UPLOAD (:obj:`int`): In bytes (10MB)
MAX_MESSAGES_PER_SECOND_PER_CHAT (:obj:`int`): `1`. Telegram may allow short bursts that go
over this limit, but eventually you'll begin receiving 429 errors.
MAX_MESSAGES_PER_SECOND (:obj:`int`): 30
@@ -47,6 +49,7 @@ MAX_CAPTION_LENGTH = 1024
SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB)
MAX_FILESIZE_UPLOAD = int(50E6) # (50MB)
MAX_PHOTOSIZE_UPLOAD = int(10E6) # (10MB)
MAX_MESSAGES_PER_SECOND_PER_CHAT = 1
MAX_MESSAGES_PER_SECOND = 30
MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20
+51 -29
View File
@@ -20,6 +20,7 @@
import logging
import warnings
from threading import Lock
from telegram import Update
from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
@@ -37,8 +38,7 @@ class _ConversationTimeoutContext(object):
class ConversationHandler(Handler):
"""
A handler to hold a conversation with a single user by managing four collections of other
handlers. Note that neither posts in Telegram Channels, nor group interactions with multiple
users are managed by instances of this class.
handlers.
The first collection, a ``list`` named :attr:`entry_points`, is used to initiate the
conversation, for example with a :class:`telegram.ext.CommandHandler` or
@@ -184,7 +184,9 @@ class ConversationHandler(Handler):
self.map_to_parent = map_to_parent
self.timeout_jobs = dict()
self._timeout_jobs_lock = Lock()
self.conversations = dict()
self._conversations_lock = Lock()
self.logger = logging.getLogger(__name__)
@@ -262,7 +264,8 @@ class ConversationHandler(Handler):
return None
key = self._get_key(update)
state = self.conversations.get(key)
with self._conversations_lock:
state = self.conversations.get(key)
# Resolve promises
if isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], Promise):
@@ -281,7 +284,8 @@ class ConversationHandler(Handler):
if res is None and old_state is None:
res = self.END
self.update_state(res, key)
state = self.conversations.get(key)
with self._conversations_lock:
state = self.conversations.get(key)
else:
handlers = self.states.get(self.WAITING, [])
for handler in handlers:
@@ -340,15 +344,22 @@ class ConversationHandler(Handler):
"""
conversation_key, handler, check_result = check_result
new_state = handler.handle_update(update, dispatcher, check_result, context)
timeout_job = self.timeout_jobs.pop(conversation_key, None)
if timeout_job is not None:
timeout_job.schedule_removal()
if self.conversation_timeout and new_state != self.END:
self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
self._trigger_timeout, self.conversation_timeout,
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
with self._timeout_jobs_lock:
# Remove the old timeout job (if present)
timeout_job = self.timeout_jobs.pop(conversation_key, None)
if timeout_job is not None:
timeout_job.schedule_removal()
new_state = handler.handle_update(update, dispatcher, check_result, context)
with self._timeout_jobs_lock:
if self.conversation_timeout and new_state != self.END:
# Add the new timeout job
self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
self._trigger_timeout, self.conversation_timeout,
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
self.update_state(self.END, conversation_key)
@@ -358,36 +369,47 @@ class ConversationHandler(Handler):
def update_state(self, new_state, key):
if new_state == self.END:
if key in self.conversations:
# If there is no key in conversations, nothing is done.
del self.conversations[key]
if self.persistent:
self.persistence.update_conversation(self.name, key, None)
with self._conversations_lock:
if key in self.conversations:
# If there is no key in conversations, nothing is done.
del self.conversations[key]
if self.persistent:
self.persistence.update_conversation(self.name, key, None)
elif isinstance(new_state, Promise):
self.conversations[key] = (self.conversations.get(key), new_state)
if self.persistent:
self.persistence.update_conversation(self.name, key,
(self.conversations.get(key), new_state))
with self._conversations_lock:
self.conversations[key] = (self.conversations.get(key), new_state)
if self.persistent:
self.persistence.update_conversation(self.name, key,
(self.conversations.get(key), new_state))
elif new_state is not None:
self.conversations[key] = new_state
if self.persistent:
self.persistence.update_conversation(self.name, key, new_state)
with self._conversations_lock:
self.conversations[key] = new_state
if self.persistent:
self.persistence.update_conversation(self.name, key, new_state)
def _trigger_timeout(self, context, job=None):
self.logger.debug('conversation timeout was triggered!')
# Backward compatibility with bots that do not use CallbackContext
callback_context = None
if isinstance(context, CallbackContext):
context = context.job.context
else:
context = job.context
job = context.job
callback_context = context
context = job.context
with self._timeout_jobs_lock:
found_job = self.timeout_jobs[context.conversation_key]
if found_job is not job:
# The timeout has been canceled in handle_update
return
del self.timeout_jobs[context.conversation_key]
del self.timeout_jobs[context.conversation_key]
handlers = self.states.get(self.TIMEOUT, [])
for handler in handlers:
check = handler.check_update(context.update)
if check is not None and check is not False:
handler.handle_update(context.update, context.dispatcher, check)
handler.handle_update(context.update, context.dispatcher, check, callback_context)
self.update_state(self.END, context.conversation_key)
+78 -2
View File
@@ -22,7 +22,7 @@ import re
from future.utils import string_types
from telegram import Chat
from telegram import Chat, Update
__all__ = ['Filters', 'BaseFilter', 'InvertedFilter', 'MergedFilter']
@@ -236,11 +236,87 @@ class Filters(object):
class _Text(BaseFilter):
name = 'Filters.text'
class _TextIterable(BaseFilter):
def __init__(self, iterable):
self.iterable = iterable
self.name = 'Filters.text({})'.format(iterable)
def filter(self, message):
if message.text and not message.text.startswith('/'):
return message.text in self.iterable
return False
def __call__(self, update):
if isinstance(update, Update):
if self.update_filter:
return self.filter(update)
else:
return self.filter(update.effective_message)
else:
return self._TextIterable(update)
def filter(self, message):
return bool(message.text and not message.text.startswith('/'))
text = _Text()
"""Text Messages."""
"""Text Messages. If an iterable of strings is passed, it filters messages to only allow those
whose text is appearing in the given iterable.
Examples:
To allow any text message, simply use
``MessageHandler(Filters.text, callback_method)``.
A simple usecase for passing an iterable is to allow only messages that were send by a
custom :class:`telegram.ReplyKeyboardMarkup`::
buttons = ['Start', 'Settings', 'Back']
markup = ReplyKeyboardMarkup.from_column(buttons)
...
MessageHandler(Filters.text(buttons), callback_method)
Args:
update (Iterable[:obj:`str`], optional): Which messages to allow. Only exact matches
are allowed. If not specified, will allow any text message.
"""
class _Caption(BaseFilter):
name = 'Filters.caption'
class _CaptionIterable(BaseFilter):
def __init__(self, iterable):
self.iterable = iterable
self.name = 'Filters.caption({})'.format(iterable)
def filter(self, message):
if message.caption:
return message.caption in self.iterable
return False
def __call__(self, update):
if isinstance(update, Update):
if self.update_filter:
return self.filter(update)
else:
return self.filter(update.effective_message)
else:
return self._CaptionIterable(update)
def filter(self, message):
return bool(message.caption)
caption = _Caption()
"""Messages with a caption. If an iterable of strings is passed, it filters messages to only
allow those whose caption is appearing in the given iterable.
Examples:
``MessageHandler(Filters.caption, callback_method)``
Args:
update (Iterable[:obj:`str`], optional): Which captions to allow. Only exact matches
are allowed. If not specified, will allow any message with a caption.
"""
class _Command(BaseFilter):
name = 'Filters.command'
+50 -35
View File
@@ -29,6 +29,7 @@ from threading import Thread, Lock, Event
from telegram.ext.callbackcontext import CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import to_float_timestamp, _UTC
class Days(object):
@@ -42,7 +43,8 @@ class JobQueue(object):
Attributes:
_queue (:obj:`PriorityQueue`): The queue that holds the Jobs.
bot (:class:`telegram.Bot`): The bot instance that should be passed to the jobs.
DEPRECATED: Use set_dispatcher instead.
DEPRECATED: Use :attr:`set_dispatcher` instead.
"""
def __init__(self, bot=None):
@@ -68,32 +70,38 @@ class JobQueue(object):
self._running = False
def set_dispatcher(self, dispatcher):
"""Set the dispatcher to be used by this JobQueue. Use this instead of passing a
:class:`telegram.Bot` to the JobQueue, which is deprecated.
Args:
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher.
"""
self._dispatcher = dispatcher
def _put(self, job, next_t=None, last_t=None):
if next_t is None:
next_t = job.interval
if next_t is None:
raise ValueError('next_t is None')
def _put(self, job, time_spec=None, previous_t=None):
"""
Enqueues the job, scheduling its next run at the correct time.
if isinstance(next_t, datetime.datetime):
next_t = (next_t - datetime.datetime.now()).total_seconds()
Args:
job (telegram.ext.Job): job to enqueue
time_spec (optional):
Specification of the time for which the job should be scheduled. The precise
semantics of this parameter depend on its type (see
:func:`telegram.ext.JobQueue.run_repeating` for details).
Defaults to now + ``job.interval``.
previous_t (optional):
Time at which the job last ran (``None`` if it hasn't run yet).
elif isinstance(next_t, datetime.time):
next_datetime = datetime.datetime.combine(datetime.date.today(), next_t)
if datetime.datetime.now().time() > next_t:
next_datetime += datetime.timedelta(days=1)
next_t = (next_datetime - datetime.datetime.now()).total_seconds()
elif isinstance(next_t, datetime.timedelta):
next_t = next_t.total_seconds()
next_t += last_t or time.time()
self.logger.debug('Putting job %s with t=%f', job.name, next_t)
"""
# get time at which to run:
time_spec = time_spec or job.interval
if time_spec is None:
raise ValueError("no time specification given for scheduling non-repeating job")
next_t = to_float_timestamp(time_spec, reference_timestamp=previous_t)
# enqueue:
self.logger.debug('Putting job %s with t=%f', job.name, time_spec)
self._queue.put((next_t, job))
# Wake up the loop if this job should be executed next
@@ -133,7 +141,7 @@ class JobQueue(object):
"""
job = Job(callback, repeat=False, context=context, name=name, job_queue=self)
self._put(job, next_t=when)
self._put(job, time_spec=when)
return job
def run_repeating(self, callback, interval, first=None, context=None, name=None):
@@ -184,7 +192,7 @@ class JobQueue(object):
context=context,
name=name,
job_queue=self)
self._put(job, next_t=first)
self._put(job, time_spec=first)
return job
def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None):
@@ -195,7 +203,8 @@ class JobQueue(object):
job. It should take ``bot, job`` as parameters, where ``job`` is the
:class:`telegram.ext.Job` instance. It can be used to access its ``Job.context``
or change it to a repeating job.
time (:obj:`datetime.time`): Time of day at which the job should run.
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``time.tzinfo``) is ``None``, UTC will be assumed.
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
run. Defaults to ``EVERY_DAY``
context (:obj:`object`, optional): Additional data needed for the callback function.
@@ -217,10 +226,11 @@ class JobQueue(object):
interval=datetime.timedelta(days=1),
repeat=True,
days=days,
tzinfo=time.tzinfo,
context=context,
name=name,
job_queue=self)
self._put(job, next_t=time)
self._put(job, time_spec=time)
return job
def _set_next_peek(self, t):
@@ -264,7 +274,7 @@ class JobQueue(object):
if job.enabled:
try:
current_week_day = datetime.datetime.now().weekday()
current_week_day = datetime.datetime.now(job.tzinfo).date().weekday()
if any(day == current_week_day for day in job.days):
self.logger.debug('Running job %s', job.name)
job.run(self._dispatcher)
@@ -276,7 +286,7 @@ class JobQueue(object):
self.logger.debug('Skipping disabled job %s', job.name)
if job.repeat and not job.removed:
self._put(job, last_t=t)
self._put(job, previous_t=t)
else:
self.logger.debug('Dropping non-repeating or removed job %s', job.name)
@@ -350,10 +360,11 @@ class Job(object):
It should take ``bot, job`` as parameters, where ``job`` is the
:class:`telegram.ext.Job` instance. It can be used to access it's :attr:`context`
or change it to a repeating job.
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`, optional): The interval in
which the job will run. If it is an :obj:`int` or a :obj:`float`, it will be
interpreted as seconds. If you don't set this value, you must set :attr:`repeat` to
``False`` and specify :attr:`next_t` when you put the job into the job queue.
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`, optional): The time
interval between executions of the job. If it is an :obj:`int` or a :obj:`float`,
it will be interpreted as seconds. If you don't set this value, you must set
:attr:`repeat` to ``False`` and specify :attr:`time_spec` when you put the job into
the job queue.
repeat (:obj:`bool`, optional): If this job should be periodically execute its callback
function (``True``) or only once (``False``). Defaults to ``True``.
context (:obj:`object`, optional): Additional data needed for the callback function. Can be
@@ -363,7 +374,9 @@ class Job(object):
Defaults to ``Days.EVERY_DAY``
job_queue (:class:`telegram.ext.JobQueue`, optional): The ``JobQueue`` this job belongs to.
Only optional for backward compatibility with ``JobQueue.put()``.
tzinfo (:obj:`datetime.tzinfo`, optional): timezone associated to this job. Used when
checking the day of the week to determine whether a job should run (only relevant when
``days is not Days.EVERY_DAY``). Defaults to UTC.
"""
def __init__(self,
@@ -373,19 +386,21 @@ class Job(object):
context=None,
days=Days.EVERY_DAY,
name=None,
job_queue=None):
job_queue=None,
tzinfo=_UTC):
self.callback = callback
self.context = context
self.name = name or callback.__name__
self._repeat = repeat
self._repeat = None
self._interval = None
self.interval = interval
self.repeat = repeat
self._days = None
self.days = days
self.tzinfo = tzinfo
self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None
+7 -14
View File
@@ -86,20 +86,13 @@ class InlineKeyboardButton(TelegramObject):
self.text = text
# Optionals
if url:
self.url = url
if login_url:
self.login_url = login_url
if callback_data:
self.callback_data = callback_data
if switch_inline_query:
self.switch_inline_query = switch_inline_query
if switch_inline_query_current_chat:
self.switch_inline_query_current_chat = switch_inline_query_current_chat
if callback_game:
self.callback_game = callback_game
if pay:
self.pay = pay
self.url = url
self.login_url = login_url
self.callback_data = callback_data
self.switch_inline_query = switch_inline_query
self.switch_inline_query_current_chat = switch_inline_query_current_chat
self.callback_game = callback_game
self.pay = pay
@classmethod
def de_json(cls, data, bot):
+7 -14
View File
@@ -77,17 +77,10 @@ class InlineQueryResultArticle(InlineQueryResult):
self.input_message_content = input_message_content
# Optional
if reply_markup:
self.reply_markup = reply_markup
if url:
self.url = url
if hide_url:
self.hide_url = hide_url
if description:
self.description = description
if thumb_url:
self.thumb_url = thumb_url
if thumb_width:
self.thumb_width = thumb_width
if thumb_height:
self.thumb_height = thumb_height
self.reply_markup = reply_markup
self.url = url
self.hide_url = hide_url
self.description = description
self.thumb_url = thumb_url
self.thumb_width = thumb_width
self.thumb_height = thumb_height
+6 -12
View File
@@ -79,15 +79,9 @@ class InlineQueryResultAudio(InlineQueryResult):
self.title = title
# Optionals
if performer:
self.performer = performer
if audio_duration:
self.audio_duration = audio_duration
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.performer = performer
self.audio_duration = audio_duration
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -68,11 +68,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
self.audio_file_id = audio_file_id
# Optionals
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -75,13 +75,8 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
self.document_file_id = document_file_id
# Optionals
if description:
self.description = description
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.description = description
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+5 -10
View File
@@ -72,13 +72,8 @@ class InlineQueryResultCachedGif(InlineQueryResult):
self.gif_file_id = gif_file_id
# Optionals
if title:
self.title = title
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.title = title
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -72,13 +72,8 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
self.mpeg4_file_id = mpeg4_file_id
# Optionals
if title:
self.title = title
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.title = title
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -75,15 +75,9 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
self.photo_file_id = photo_file_id
# Optionals
if title:
self.title = title
if description:
self.description = description
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.title = title
self.description = description
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -58,7 +58,5 @@ class InlineQueryResultCachedSticker(InlineQueryResult):
self.sticker_file_id = sticker_file_id
# Optionals
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -76,13 +76,8 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
self.title = title
# Optionals
if description:
self.description = description
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.description = description
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -72,11 +72,7 @@ class InlineQueryResultCachedVoice(InlineQueryResult):
self.title = title
# Optionals
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+7 -14
View File
@@ -79,17 +79,10 @@ class InlineQueryResultContact(InlineQueryResult):
self.first_name = first_name
# Optionals
if last_name:
self.last_name = last_name
if vcard:
self.vcard = vcard
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
if thumb_url:
self.thumb_url = thumb_url
if thumb_width:
self.thumb_width = thumb_width
if thumb_height:
self.thumb_height = thumb_height
self.last_name = last_name
self.vcard = vcard
self.reply_markup = reply_markup
self.input_message_content = input_message_content
self.thumb_url = thumb_url
self.thumb_width = thumb_width
self.thumb_height = thumb_height
+8 -16
View File
@@ -91,19 +91,11 @@ class InlineQueryResultDocument(InlineQueryResult):
self.mime_type = mime_type
# Optionals
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if description:
self.description = description
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
if thumb_url:
self.thumb_url = thumb_url
if thumb_width:
self.thumb_width = thumb_width
if thumb_height:
self.thumb_height = thumb_height
self.caption = caption
self.parse_mode = parse_mode
self.description = description
self.reply_markup = reply_markup
self.input_message_content = input_message_content
self.thumb_url = thumb_url
self.thumb_width = thumb_width
self.thumb_height = thumb_height
+1 -2
View File
@@ -46,5 +46,4 @@ class InlineQueryResultGame(InlineQueryResult):
self.id = id
self.game_short_name = game_short_name
if reply_markup:
self.reply_markup = reply_markup
self.reply_markup = reply_markup
+8 -16
View File
@@ -85,19 +85,11 @@ class InlineQueryResultGif(InlineQueryResult):
self.thumb_url = thumb_url
# Optionals
if gif_width:
self.gif_width = gif_width
if gif_height:
self.gif_height = gif_height
if gif_duration:
self.gif_duration = gif_duration
if title:
self.title = title
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.gif_width = gif_width
self.gif_height = gif_height
self.gif_duration = gif_duration
self.title = title
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+6 -12
View File
@@ -80,15 +80,9 @@ class InlineQueryResultLocation(InlineQueryResult):
self.title = title
# Optionals
if live_period:
self.live_period = live_period
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
if thumb_url:
self.thumb_url = thumb_url
if thumb_width:
self.thumb_width = thumb_width
if thumb_height:
self.thumb_height = thumb_height
self.live_period = live_period
self.reply_markup = reply_markup
self.input_message_content = input_message_content
self.thumb_url = thumb_url
self.thumb_width = thumb_width
self.thumb_height = thumb_height
+8 -16
View File
@@ -86,19 +86,11 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
self.thumb_url = thumb_url
# Optional
if mpeg4_width:
self.mpeg4_width = mpeg4_width
if mpeg4_height:
self.mpeg4_height = mpeg4_height
if mpeg4_duration:
self.mpeg4_duration = mpeg4_duration
if title:
self.title = title
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.mpeg4_width = mpeg4_width
self.mpeg4_height = mpeg4_height
self.mpeg4_duration = mpeg4_duration
self.title = title
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+8 -16
View File
@@ -86,19 +86,11 @@ class InlineQueryResultPhoto(InlineQueryResult):
self.thumb_url = thumb_url
# Optionals
if photo_width:
self.photo_width = int(photo_width)
if photo_height:
self.photo_height = int(photo_height)
if title:
self.title = title
if description:
self.description = description
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.photo_width = int(photo_width)if photo_width is not None else None
self.photo_height = int(photo_height) if photo_height is not None else None
self.title = title
self.description = description
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+7 -14
View File
@@ -90,17 +90,10 @@ class InlineQueryResultVenue(InlineQueryResult):
self.address = address
# Optional
if foursquare_id:
self.foursquare_id = foursquare_id
if foursquare_type:
self.foursquare_type = foursquare_type
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
if thumb_url:
self.thumb_url = thumb_url
if thumb_width:
self.thumb_width = thumb_width
if thumb_height:
self.thumb_height = thumb_height
self.foursquare_id = foursquare_id
self.foursquare_type = foursquare_type
self.reply_markup = reply_markup
self.input_message_content = input_message_content
self.thumb_url = thumb_url
self.thumb_width = thumb_width
self.thumb_height = thumb_height
+8 -16
View File
@@ -94,19 +94,11 @@ class InlineQueryResultVideo(InlineQueryResult):
self.title = title
# Optional
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if video_width:
self.video_width = video_width
if video_height:
self.video_height = video_height
if video_duration:
self.video_duration = video_duration
if description:
self.description = description
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.caption = caption
self.parse_mode = parse_mode
self.video_width = video_width
self.video_height = video_height
self.video_duration = video_duration
self.description = description
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+5 -10
View File
@@ -77,13 +77,8 @@ class InlineQueryResultVoice(InlineQueryResult):
self.title = title
# Optional
if voice_duration:
self.voice_duration = voice_duration
if caption:
self.caption = caption
if parse_mode:
self.parse_mode = parse_mode
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:
self.input_message_content = input_message_content
self.voice_duration = voice_duration
self.caption = caption
self.parse_mode = parse_mode
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+5 -7
View File
@@ -54,13 +54,11 @@ class LoginUrl(TelegramObject):
"""
def __init__(self, url, forward_text=None, bot_username=None, request_write_access=None):
# Required
self.url = url
if forward_text:
self.forward_text = forward_text
if bot_username:
self.bot_username = bot_username
if request_write_access:
self.request_write_access = request_write_access
# Optional
self.forward_text = forward_text
self.bot_username = bot_username
self.request_write_access = request_write_access
self._id_attrs = (self.url,)
+30 -9
View File
@@ -338,10 +338,20 @@ class Message(TelegramObject):
@property
def link(self):
""":obj:`str`: Convenience property. If the chat of the message is a supergroup or a
channel and has a :attr:`Chat.username`, returns a t.me link of the message."""
if self.chat.type in (Chat.SUPERGROUP, Chat.CHANNEL) and self.chat.username:
return "https://t.me/{}/{}".format(self.chat.username, self.message_id)
""":obj:`str`: Convenience property. If the chat of the message is not
a private chat, returns a t.me link of the message."""
if self.chat.type != Chat.PRIVATE:
if self.chat.username:
to_link = self.chat.username
else:
if self.chat.type != Chat.GROUP:
# Get rid of leading -100 for supergroups
id_to_link = str(self.chat.id)[4:]
else:
# Get rid of leading minus for regular groups
id_to_link = str(self.chat.id)[1:]
to_link = "c/{}".format(id_to_link)
return "https://t.me/{}/{}".format(to_link, self.message_id)
return None
@classmethod
@@ -473,6 +483,9 @@ class Message(TelegramObject):
parameter will be ignored. Default: ``True`` in group chats and ``False`` in
private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
self._quote(kwargs)
return self.bot.send_message(self.chat_id, *args, **kwargs)
@@ -490,6 +503,9 @@ class Message(TelegramObject):
reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
parameter will be ignored. Default: ``True`` in group chats and ``False`` in
private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
kwargs['parse_mode'] = ParseMode.MARKDOWN
@@ -510,6 +526,9 @@ class Message(TelegramObject):
reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this
parameter will be ignored. Default: ``True`` in group chats and ``False`` in
private chats.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
kwargs['parse_mode'] = ParseMode.HTML
@@ -742,13 +761,14 @@ class Message(TelegramObject):
self._quote(kwargs)
return self.bot.send_poll(self.chat_id, *args, **kwargs)
def forward(self, chat_id, disable_notification=False):
def forward(self, chat_id, *args, **kwargs):
"""Shortcut for::
bot.forward_message(chat_id=chat_id,
from_chat_id=update.message.chat_id,
disable_notification=disable_notification,
message_id=update.message.message_id)
message_id=update.message.message_id,
*args,
**kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message forwarded.
@@ -757,8 +777,9 @@ class Message(TelegramObject):
return self.bot.forward_message(
chat_id=chat_id,
from_chat_id=self.chat_id,
disable_notification=disable_notification,
message_id=self.message_id)
message_id=self.message_id,
*args,
**kwargs)
def edit_text(self, *args, **kwargs):
"""Shortcut for::
+130 -26
View File
@@ -17,7 +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 helper functions."""
import datetime as dtm # dtm = "DateTime Module"
import time
from collections import defaultdict
from numbers import Number
try:
import ujson as json
@@ -27,7 +32,6 @@ from html import escape
import re
import signal
from datetime import datetime
# From https://stackoverflow.com/questions/2549939/get-signal-names-from-numbers-in-python
_signames = {v: k
@@ -40,54 +44,154 @@ def get_signal_name(signum):
return _signames[signum]
# Not using future.backports.datetime here as datetime value might be an input from the user,
# making every isinstace() call more delicate. So we just use our own compat layer.
if hasattr(datetime, 'timestamp'):
# Python 3.3+
def _timestamp(dt_obj):
return dt_obj.timestamp()
else:
# Python < 3.3 (incl 2.7)
from time import mktime
def _timestamp(dt_obj):
return mktime(dt_obj.timetuple())
def escape_markdown(text):
"""Helper function to escape telegram markup symbols."""
escape_chars = '\*_`\['
return re.sub(r'([%s])' % escape_chars, r'\\\1', text)
def to_timestamp(dt_obj):
# -------- date/time related helpers --------
# TODO: add generic specification of UTC for naive datetimes to docs
if hasattr(dtm, 'timezone'):
# Python 3.3+
def _datetime_to_float_timestamp(dt_obj):
if dt_obj.tzinfo is None:
dt_obj = dt_obj.replace(tzinfo=_UTC)
return dt_obj.timestamp()
_UtcOffsetTimezone = dtm.timezone
_UTC = dtm.timezone.utc
else:
# Python < 3.3 (incl 2.7)
# hardcoded timezone class (`datetime.timezone` isn't available in py2)
class _UtcOffsetTimezone(dtm.tzinfo):
def __init__(self, offset):
self.offset = offset
def tzname(self, dt):
return 'UTC +{}'.format(self.offset)
def utcoffset(self, dt):
return self.offset
def dst(self, dt):
return dtm.timedelta(0)
_UTC = _UtcOffsetTimezone(dtm.timedelta(0))
__EPOCH_DT = dtm.datetime.fromtimestamp(0, tz=_UTC)
__NAIVE_EPOCH_DT = __EPOCH_DT.replace(tzinfo=None)
# _datetime_to_float_timestamp
# Not using future.backports.datetime here as datetime value might be an input from the user,
# making every isinstace() call more delicate. So we just use our own compat layer.
def _datetime_to_float_timestamp(dt_obj):
epoch_dt = __EPOCH_DT if dt_obj.tzinfo is not None else __NAIVE_EPOCH_DT
return (dt_obj - epoch_dt).total_seconds()
_datetime_to_float_timestamp.__doc__ = \
"""Converts a datetime object to a float timestamp (with sub-second precision).
If the datetime object is timezone-naive, it is assumed to be in UTC."""
def to_float_timestamp(t, reference_timestamp=None):
"""
Converts a given time object to a float POSIX timestamp.
Used to convert different time specifications to a common format. The time object
can be relative (i.e. indicate a time increment, or a time of day) or absolute.
Any objects from the :module:`datetime` module that are timezone-naive will be assumed
to be in UTC.
``None`` s are left alone (i.e. ``to_float_timestamp(None)`` is ``None``).
Args:
dt_obj (:class:`datetime.datetime`):
t (int | float | datetime.timedelta | datetime.datetime | datetime.time):
Time value to convert. The semantics of this parameter will depend on its type:
* :obj:`int` or :obj:`float` will be interpreted as "seconds from ``reference_t``"
* :obj:`datetime.timedelta` will be interpreted as
"time increment from ``reference_t``"
* :obj:`datetime.datetime` will be interpreted as an absolute date/time value
* :obj:`datetime.time` will be interpreted as a specific time of day
reference_timestamp (float, optional): POSIX timestamp that indicates the absolute time
from which relative calculations are to be performed (e.g. when ``t`` is given as an
:obj:`int`, indicating "seconds from ``reference_t``"). Defaults to now (the time at
which this function is called).
If ``t`` is given as an absolute representation of date & time (i.e. a
``datetime.datetime`` object), ``reference_timestamp`` is not relevant and so its
value should be ``None``. If this is not the case, a ``ValueError`` will be raised.
Returns:
int:
(float | None) The return value depends on the type of argument ``t``. If ``t`` is
given as a time increment (i.e. as a obj:`int`, :obj:`float` or
:obj:`datetime.timedelta`), then the return value will be ``reference_t`` + ``t``.
Else if it is given as an absolute date/time value (i.e. a :obj:`datetime.datetime`
object), the equivalent value as a POSIX timestamp will be returned.
Finally, if it is a time of the day without date (i.e. a :obj:`datetime.time`
object), the return value is the nearest future occurrence of that time of day.
Raises:
TypeError: if `t`'s type is not one of those described above
"""
if not dt_obj:
return None
return int(_timestamp(dt_obj))
if reference_timestamp is None:
reference_timestamp = time.time()
elif isinstance(t, dtm.datetime):
raise ValueError('t is an (absolute) datetime while reference_timestamp is not None')
if isinstance(t, dtm.timedelta):
return reference_timestamp + t.total_seconds()
elif isinstance(t, Number):
return reference_timestamp + t
elif isinstance(t, dtm.time):
if t.tzinfo is not None:
reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo)
else:
reference_dt = dtm.datetime.utcfromtimestamp(reference_timestamp) # assume UTC
reference_date = reference_dt.date()
reference_time = reference_dt.timetz()
if reference_time > t: # if the time of day has passed today, use tomorrow
reference_date += dtm.timedelta(days=1)
return _datetime_to_float_timestamp(dtm.datetime.combine(reference_date, t))
elif isinstance(t, dtm.datetime):
return _datetime_to_float_timestamp(t)
raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__))
def to_timestamp(dt_obj, reference_timestamp=None):
"""
Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated
down to the nearest integer).
See the documentation for :func:`to_float_timestamp` for more details.
"""
return int(to_float_timestamp(dt_obj, reference_timestamp)) if dt_obj is not None else None
def from_timestamp(unixtime):
"""
Converts an (integer) unix timestamp to a naive datetime object in UTC.
``None`` s are left alone (i.e. ``from_timestamp(None)`` is ``None``).
Args:
unixtime (int):
unixtime (int): integer POSIX timestamp
Returns:
datetime.datetime:
equivalent :obj:`datetime.datetime` value in naive UTC if ``timestamp`` is not
``None``; else ``None``
"""
if not unixtime:
if unixtime is None:
return None
return datetime.utcfromtimestamp(unixtime)
return dtm.datetime.utcfromtimestamp(unixtime)
# -------- end --------
def mention_html(user_id, name):
+1 -1
View File
@@ -17,4 +17,4 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
__version__ = '12.2.0'
__version__ = '12.3.0'
+25 -16
View File
@@ -17,11 +17,10 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Provide a bot to tests"""
import json
import base64
import os
import random
import sys
from platform import python_implementation
# Provide some public fallbacks so it's easy for contributors to run tests on their local machine
# These bots are only able to talk in our test chats, so they are quite useless for other
@@ -32,32 +31,42 @@ FALLBACKS = [
'payment_provider_token': '284685063:TEST:NjQ0NjZlNzI5YjJi',
'chat_id': '675666224',
'super_group_id': '-1001493296829',
'channel_id': '@pythontelegrambottests'
'channel_id': '@pythontelegrambottests',
'bot_name': 'PTB tests fallback 1',
'bot_username': '@ptb_fallback_1_bot'
}, {
'token': '558194066:AAEEylntuKSLXj9odiv3TnX7Z5KY2J3zY3M',
'payment_provider_token': '284685063:TEST:YjEwODQwMTFmNDcy',
'chat_id': '675666224',
'super_group_id': '-1001493296829',
'channel_id': '@pythontelegrambottests'
'channel_id': '@pythontelegrambottests',
'bot_name': 'PTB tests fallback 2',
'bot_username': '@ptb_fallback_2_bot'
}
]
GITHUB_ACTION = os.getenv('GITHUB_ACTION', None)
BOTS = os.getenv('BOTS', None)
JOB_INDEX = os.getenv('JOB_INDEX', None)
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
BOTS = json.loads(base64.b64decode(BOTS).decode('utf-8'))
JOB_INDEX = int(JOB_INDEX)
def get(name, fallback):
full_name = '{0}_{1}_{2[0]}{2[1]}'.format(name, python_implementation(),
sys.version_info).upper()
# First try full_names such as
# TOKEN_CPYTHON_33
# CHAT_ID_PYPY_27
val = os.getenv(full_name)
if val:
return val
# Then try short names
# TOKEN
# CHAT_ID
# If we have TOKEN, PAYMENT_PROVIDER_TOKEN, CHAT_ID, SUPER_GROUP_ID,
# CHANNEL_ID, BOT_NAME, or BOT_USERNAME in the environment, then use that
val = os.getenv(name.upper())
if val:
return val
# If we're running as a github action then fetch bots from the repo secrets
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
try:
return BOTS[JOB_INDEX][name]
except KeyError:
pass
# Otherwise go with the fallback
return fallback
+19 -2
View File
@@ -27,9 +27,11 @@ from time import sleep
import pytest
from telegram import Bot, Message, User, Chat, MessageEntity, Update, \
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery, ChosenInlineResult
from telegram import (Bot, Message, User, Chat, MessageEntity, Update,
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery,
ChosenInlineResult)
from telegram.ext import Dispatcher, JobQueue, Updater, BaseFilter
from telegram.utils.helpers import _UtcOffsetTimezone
from tests.bots import get_bot
TRAVIS = os.getenv('TRAVIS', False)
@@ -37,6 +39,11 @@ TRAVIS = os.getenv('TRAVIS', False)
if TRAVIS:
pytest_plugins = ['tests.travis_fold']
GITHUB_ACTION = os.getenv('GITHUB_ACTION', False)
if GITHUB_ACTION:
pytest_plugins = ['tests.plugin_github_group']
# THIS KEY IS OBVIOUSLY COMPROMISED
# DO NOT USE IN PRODUCTION!
PRIVATE_KEY = b"-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA0AvEbNaOnfIL3GjB8VI4M5IaWe+GcK8eSPHkLkXREIsaddum\r\nwPBm/+w8lFYdnY+O06OEJrsaDtwGdU//8cbGJ/H/9cJH3dh0tNbfszP7nTrQD+88\r\nydlcYHzClaG8G+oTe9uEZSVdDXj5IUqR0y6rDXXb9tC9l+oSz+ShYg6+C4grAb3E\r\nSTv5khZ9Zsi/JEPWStqNdpoNuRh7qEYc3t4B/a5BH7bsQENyJSc8AWrfv+drPAEe\r\njQ8xm1ygzWvJp8yZPwOIYuL+obtANcoVT2G2150Wy6qLC0bD88Bm40GqLbSazueC\r\nRHZRug0B9rMUKvKc4FhG4AlNzBCaKgIcCWEqKwIDAQABAoIBACcIjin9d3Sa3S7V\r\nWM32JyVF3DvTfN3XfU8iUzV7U+ZOswA53eeFM04A/Ly4C4ZsUNfUbg72O8Vd8rg/\r\n8j1ilfsYpHVvphwxaHQlfIMa1bKCPlc/A6C7b2GLBtccKTbzjARJA2YWxIaqk9Nz\r\nMjj1IJK98i80qt29xRnMQ5sqOO3gn2SxTErvNchtBiwOH8NirqERXig8VCY6fr3n\r\nz7ZImPU3G/4qpD0+9ULrt9x/VkjqVvNdK1l7CyAuve3D7ha3jPMfVHFtVH5gqbyp\r\nKotyIHAyD+Ex3FQ1JV+H7DkP0cPctQiss7OiO9Zd9C1G2OrfQz9el7ewAPqOmZtC\r\nKjB3hUECgYEA/4MfKa1cvaCqzd3yUprp1JhvssVkhM1HyucIxB5xmBcVLX2/Kdhn\r\nhiDApZXARK0O9IRpFF6QVeMEX7TzFwB6dfkyIePsGxputA5SPbtBlHOvjZa8omMl\r\nEYfNa8x/mJkvSEpzvkWPascuHJWv1cEypqphu/70DxubWB5UKo/8o6cCgYEA0HFy\r\ncgwPMB//nltHGrmaQZPFT7/Qgl9ErZT3G9S8teWY4o4CXnkdU75tBoKAaJnpSfX3\r\nq8VuRerF45AFhqCKhlG4l51oW7TUH50qE3GM+4ivaH5YZB3biwQ9Wqw+QyNLAh/Q\r\nnS4/Wwb8qC9QuyEgcCju5lsCaPEXZiZqtPVxZd0CgYEAshBG31yZjO0zG1TZUwfy\r\nfN3euc8mRgZpSdXIHiS5NSyg7Zr8ZcUSID8jAkJiQ3n3OiAsuq1MGQ6kNa582kLT\r\nFPQdI9Ea8ahyDbkNR0gAY9xbM2kg/Gnro1PorH9PTKE0ekSodKk1UUyNrg4DBAwn\r\nqE6E3ebHXt/2WmqIbUD653ECgYBQCC8EAQNX3AFegPd1GGxU33Lz4tchJ4kMCNU0\r\nN2NZh9VCr3nTYjdTbxsXU8YP44CCKFG2/zAO4kymyiaFAWEOn5P7irGF/JExrjt4\r\nibGy5lFLEq/HiPtBjhgsl1O0nXlwUFzd7OLghXc+8CPUJaz5w42unqT3PBJa40c3\r\nQcIPdQKBgBnSb7BcDAAQ/Qx9juo/RKpvhyeqlnp0GzPSQjvtWi9dQRIu9Pe7luHc\r\nm1Img1EO1OyE3dis/rLaDsAa2AKu1Yx6h85EmNjavBqP9wqmFa0NIQQH8fvzKY3/\r\nP8IHY6009aoamLqYaexvrkHVq7fFKiI6k8myMJ6qblVNFv14+KXU\r\n-----END RSA PRIVATE KEY-----" # noqa: E501
@@ -253,3 +260,13 @@ def get_false_update_fixture_decorator_params():
@pytest.fixture(scope='function', **get_false_update_fixture_decorator_params())
def false_update(request):
return Update(update_id=1, **request.param)
@pytest.fixture(params=[1, 2], ids=lambda h: 'UTC +{hour:0>2}:00'.format(hour=h))
def utc_offset(request):
return datetime.timedelta(hours=request.param)
@pytest.fixture()
def timezone(utc_offset):
return _UtcOffsetTimezone(utc_offset)
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# 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/].
import _pytest.config
import pytest
fold_plugins = {'_cov': 'Coverage report', 'flaky': 'Flaky report'}
def terminal_summary_wrapper(original, plugin_name):
text = fold_plugins[plugin_name]
def pytest_terminal_summary(terminalreporter):
terminalreporter.write('##[group] {}\n'.format(text))
original(terminalreporter)
terminalreporter.write('##[endgroup]')
return pytest_terminal_summary
@pytest.mark.trylast
def pytest_configure(config):
for hookimpl in config.pluginmanager.hook.pytest_terminal_summary._nonwrappers:
if hookimpl.plugin_name in fold_plugins.keys():
hookimpl.function = terminal_summary_wrapper(hookimpl.function,
hookimpl.plugin_name)
terminal = None
previous_name = None
def _get_name(location):
if location[0].startswith('tests/'):
return location[0][6:]
return location[0]
@pytest.mark.trylast
def pytest_itemcollected(item):
item._nodeid = item._nodeid.split('::', 1)[1]
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_protocol(item, nextitem):
# This is naughty but pytests' own plugins does something similar too, so who cares
global terminal
if terminal is None:
terminal = _pytest.config.create_terminal_writer(item.config)
global previous_name
name = _get_name(item.location)
if previous_name is None or previous_name != name:
previous_name = name
terminal.write('\n##[group] {}'.format(name))
yield
if nextitem is None or _get_name(nextitem.location) != name:
terminal.write('\n##[endgroup]')
+5 -4
View File
@@ -19,7 +19,7 @@
import os
import sys
import time
from datetime import datetime
import datetime as dtm
from platform import python_implementation
import pytest
@@ -108,19 +108,20 @@ class TestBot(object):
assert message.text == message.text
assert message.forward_from.username == message.from_user.username
assert isinstance(message.forward_date, datetime)
assert isinstance(message.forward_date, dtm.datetime)
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_delete_message(self, bot, chat_id):
message = bot.send_message(chat_id, text='will be deleted')
time.sleep(2)
assert bot.delete_message(chat_id=chat_id, message_id=message.message_id) is True
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_delete_message_old_message(self, bot, chat_id):
with pytest.raises(TelegramError, match='Message to delete not found'):
with pytest.raises(BadRequest):
# Considering that the first message is old enough
bot.delete_message(chat_id=chat_id, message_id=1)
@@ -614,7 +615,7 @@ class TestBot(object):
assert bot.restrict_chat_member(channel_id,
95205500,
chat_permissions,
until_date=datetime.now())
until_date=dtm.datetime.utcnow())
@flaky(3, 1)
@pytest.mark.timeout(10)
+1 -1
View File
@@ -46,7 +46,7 @@ class TestChatMember(object):
assert chat_member.status == self.status
def test_de_json_all_args(self, bot, user):
time = datetime.datetime.now()
time = datetime.datetime.utcnow()
json_dict = {'user': user.to_dict(),
'status': self.status,
'until_date': to_timestamp(time),
+119 -7
View File
@@ -24,7 +24,7 @@ import pytest
from telegram import (CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message,
PreCheckoutQuery, ShippingQuery, Update, User, MessageEntity)
from telegram.ext import (ConversationHandler, CommandHandler, CallbackQueryHandler,
MessageHandler, Filters, InlineQueryHandler)
MessageHandler, Filters, InlineQueryHandler, CallbackContext)
@pytest.fixture(scope='class')
@@ -117,7 +117,10 @@ class TestConversationHandler(object):
# Actions
def start(self, bot, update):
return self._set_state(update, self.THIRSTY)
if isinstance(update, Update):
return self._set_state(update, self.THIRSTY)
else:
return self._set_state(bot, self.THIRSTY)
def end(self, bot, update):
return self._set_state(update, self.END)
@@ -129,7 +132,10 @@ class TestConversationHandler(object):
return self._set_state(update, None)
def brew(self, bot, update):
return self._set_state(update, self.BREWING)
if isinstance(update, Update):
return self._set_state(update, self.BREWING)
else:
return self._set_state(bot, self.BREWING)
def drink(self, bot, update):
return self._set_state(update, self.DRINKING)
@@ -139,9 +145,20 @@ class TestConversationHandler(object):
def passout(self, bot, update):
assert update.message.text == '/brew'
assert isinstance(update, Update)
self.is_timeout = True
def passout2(self, bot, update):
assert isinstance(update, Update)
self.is_timeout = True
def passout_context(self, update, context):
assert update.message.text == '/brew'
assert isinstance(context, CallbackContext)
self.is_timeout = True
def passout2_context(self, update, context):
assert isinstance(context, CallbackContext)
self.is_timeout = True
# Drinking actions (nested)
@@ -613,6 +630,101 @@ class TestConversationHandler(object):
assert handler.conversations.get((self.group.id, user1.id)) is None
assert not self.is_timeout
def test_conversation_handler_timeout_state_context(self, cdp, bot, user1):
states = self.states
states.update({ConversationHandler.TIMEOUT: [
CommandHandler('brew', self.passout_context),
MessageHandler(~Filters.regex('oding'), self.passout2_context)
]})
handler = ConversationHandler(entry_points=self.entry_points, states=states,
fallbacks=self.fallbacks, conversation_timeout=0.5)
cdp.add_handler(handler)
# CommandHandler timeout
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0,
length=len('/start'))],
bot=bot)
cdp.process_update(Update(update_id=0, message=message))
message.text = '/brew'
message.entities[0].length = len('/brew')
cdp.process_update(Update(update_id=0, message=message))
sleep(0.5)
cdp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert self.is_timeout
# MessageHandler timeout
self.is_timeout = False
message.text = '/start'
message.entities[0].length = len('/start')
cdp.process_update(Update(update_id=1, message=message))
sleep(0.5)
cdp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert self.is_timeout
# Timeout but no valid handler
self.is_timeout = False
cdp.process_update(Update(update_id=0, message=message))
message.text = '/brew'
message.entities[0].length = len('/brew')
cdp.process_update(Update(update_id=0, message=message))
message.text = '/startCoding'
message.entities[0].length = len('/startCoding')
cdp.process_update(Update(update_id=0, message=message))
sleep(0.5)
cdp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert not self.is_timeout
def test_conversation_timeout_cancel_conflict(self, dp, bot, user1):
# Start state machine, wait half the timeout,
# then call a callback that takes more than the timeout
# t=0 /start (timeout=.5)
# t=.25 /slowbrew (sleep .5)
# | t=.5 original timeout (should not execute)
# | t=.75 /slowbrew returns (timeout=1.25)
# t=1.25 timeout
def slowbrew(_bot, update):
sleep(0.25)
# Let's give to the original timeout a chance to execute
dp.job_queue.tick()
sleep(0.25)
# By returning None we do not override the conversation state so
# we can see if the timeout has been executed
states = self.states
states[self.THIRSTY].append(CommandHandler('slowbrew', slowbrew))
states.update({ConversationHandler.TIMEOUT: [
MessageHandler(None, self.passout2)
]})
handler = ConversationHandler(entry_points=self.entry_points, states=states,
fallbacks=self.fallbacks, conversation_timeout=0.5)
dp.add_handler(handler)
# CommandHandler timeout
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0,
length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
sleep(0.25)
dp.job_queue.tick()
message.text = '/slowbrew'
message.entities[0].length = len('/slowbrew')
dp.process_update(Update(update_id=0, message=message))
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is not None
assert not self.is_timeout
sleep(0.5)
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert self.is_timeout
def test_per_message_warning_is_only_shown_once(self, recwarn):
ConversationHandler(
entry_points=self.entry_points,
@@ -670,10 +782,10 @@ class TestConversationHandler(object):
def test_nested_conversation_handler(self, dp, bot, user1, user2):
self.nested_states[self.DRINKING] = [ConversationHandler(
entry_points=self.drinking_entry_points,
states=self.drinking_states,
fallbacks=self.drinking_fallbacks,
map_to_parent=self.drinking_map_to_parent)]
entry_points=self.drinking_entry_points,
states=self.drinking_states,
fallbacks=self.drinking_fallbacks,
map_to_parent=self.drinking_map_to_parent)]
handler = ConversationHandler(entry_points=self.entry_points,
states=self.nested_states,
fallbacks=self.fallbacks)
+25 -9
View File
@@ -27,7 +27,7 @@ import re
@pytest.fixture(scope='function')
def update():
return Update(0, Message(0, User(0, 'Testuser', False), datetime.datetime.now(),
return Update(0, Message(0, User(0, 'Testuser', False), datetime.datetime.utcnow(),
Chat(0, 'private')))
@@ -43,9 +43,25 @@ class TestFilters(object):
def test_filters_text(self, update):
update.message.text = 'test'
assert Filters.text(update)
assert (Filters.text)(update)
update.message.text = '/test'
assert not Filters.text(update)
assert not (Filters.text)(update)
def test_filters_text_iterable(self, update):
update.message.text = 'test'
assert Filters.text({'test', 'test1'})(update)
assert not Filters.text(['test1', 'test2'])(update)
def test_filters_caption(self, update):
update.message.caption = 'test'
assert (Filters.caption)(update)
update.message.caption = None
assert not (Filters.caption)(update)
def test_filters_caption_iterable(self, update):
update.message.caption = 'test'
assert Filters.caption({'test', 'test1'})(update)
assert not Filters.caption(['test1', 'test2'])(update)
def test_filters_command(self, update):
update.message.text = 'test'
@@ -138,7 +154,7 @@ class TestFilters(object):
assert isinstance(matches, list)
assert len(matches) == 2
assert all([type(res) == SRE_TYPE for res in matches])
update.message.forward_date = datetime.datetime.now()
update.message.forward_date = datetime.datetime.utcnow()
result = filter(update)
assert result
assert isinstance(result, dict)
@@ -248,7 +264,7 @@ class TestFilters(object):
assert result
def test_filters_reply(self, update):
another_message = Message(1, User(1, 'TestOther', False), datetime.datetime.now(),
another_message = Message(1, User(1, 'TestOther', False), datetime.datetime.utcnow(),
Chat(0, 'private'))
update.message.text = 'test'
assert not Filters.reply(update)
@@ -475,7 +491,7 @@ class TestFilters(object):
def test_filters_forwarded(self, update):
assert not Filters.forwarded(update)
update.message.forward_date = datetime.datetime.now()
update.message.forward_date = datetime.datetime.utcnow()
assert Filters.forwarded(update)
def test_filters_game(self, update):
@@ -606,7 +622,7 @@ class TestFilters(object):
def test_and_filters(self, update):
update.message.text = 'test'
update.message.forward_date = datetime.datetime.now()
update.message.forward_date = datetime.datetime.utcnow()
assert (Filters.text & Filters.forwarded)(update)
update.message.text = '/test'
assert not (Filters.text & Filters.forwarded)(update)
@@ -615,7 +631,7 @@ class TestFilters(object):
assert not (Filters.text & Filters.forwarded)(update)
update.message.text = 'test'
update.message.forward_date = datetime.datetime.now()
update.message.forward_date = datetime.datetime.utcnow()
assert (Filters.text & Filters.forwarded & Filters.private)(update)
def test_or_filters(self, update):
@@ -630,7 +646,7 @@ class TestFilters(object):
def test_and_or_filters(self, update):
update.message.text = 'test'
update.message.forward_date = datetime.datetime.now()
update.message.forward_date = datetime.datetime.utcnow()
assert (Filters.text & (Filters.status_update | Filters.forwarded))(update)
update.message.forward_date = False
assert not (Filters.text & (Filters.forwarded | Filters.status_update))(update)
+84 -1
View File
@@ -16,6 +16,9 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import time
import datetime as dtm
import pytest
from telegram import Sticker
@@ -23,6 +26,17 @@ from telegram import Update
from telegram import User
from telegram.message import Message
from telegram.utils import helpers
from telegram.utils.helpers import _UtcOffsetTimezone, _datetime_to_float_timestamp
# sample time specification values categorised into absolute / delta / time-of-day
ABSOLUTE_TIME_SPECS = [dtm.datetime.now(tz=_UtcOffsetTimezone(dtm.timedelta(hours=-7))),
dtm.datetime.utcnow()]
DELTA_TIME_SPECS = [dtm.timedelta(hours=3, seconds=42, milliseconds=2), 30, 7.5]
TIME_OF_DAY_TIME_SPECS = [dtm.time(12, 42, tzinfo=_UtcOffsetTimezone(dtm.timedelta(hours=-7))),
dtm.time(12, 42)]
RELATIVE_TIME_SPECS = DELTA_TIME_SPECS + TIME_OF_DAY_TIME_SPECS
TIME_SPECS = ABSOLUTE_TIME_SPECS + RELATIVE_TIME_SPECS
class TestHelpers(object):
@@ -32,6 +46,76 @@ class TestHelpers(object):
assert expected_str == helpers.escape_markdown(test_str)
def test_to_float_timestamp_absolute_naive(self):
"""Conversion from timezone-naive datetime to timestamp.
Naive datetimes should be assumed to be in UTC.
"""
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
assert helpers.to_float_timestamp(datetime) == 1573431976.1
def test_to_float_timestamp_absolute_aware(self, timezone):
"""Conversion from timezone-aware datetime to timestamp"""
# we're parametrizing this with two different UTC offsets to exclude the possibility
# of an xpass when the test is run in a timezone with the same UTC offset
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5, tzinfo=timezone)
assert (helpers.to_float_timestamp(datetime)
== 1573431976.1 - timezone.utcoffset(None).total_seconds())
def test_to_float_timestamp_absolute_no_reference(self):
"""A reference timestamp is only relevant for relative time specifications"""
with pytest.raises(ValueError):
helpers.to_float_timestamp(dtm.datetime(2019, 11, 11), reference_timestamp=123)
@pytest.mark.parametrize('time_spec', DELTA_TIME_SPECS, ids=str)
def test_to_float_timestamp_delta(self, time_spec):
"""Conversion from a 'delta' time specification to timestamp"""
reference_t = 0
delta = time_spec.total_seconds() if hasattr(time_spec, 'total_seconds') else time_spec
assert helpers.to_float_timestamp(time_spec, reference_t) == reference_t + delta
def test_to_float_timestamp_time_of_day(self):
"""Conversion from time-of-day specification to timestamp"""
hour, hour_delta = 12, 1
ref_t = _datetime_to_float_timestamp(dtm.datetime(1970, 1, 1, hour=hour))
# test for a time of day that is still to come, and one in the past
time_future, time_past = dtm.time(hour + hour_delta), dtm.time(hour - hour_delta)
assert helpers.to_float_timestamp(time_future, ref_t) == ref_t + 60 * 60 * hour_delta
assert helpers.to_float_timestamp(time_past, ref_t) == ref_t + 60 * 60 * (24 - hour_delta)
def test_to_float_timestamp_time_of_day_timezone(self, timezone):
"""Conversion from timezone-aware time-of-day specification to timestamp"""
# we're parametrizing this with two different UTC offsets to exclude the possibility
# of an xpass when the test is run in a timezone with the same UTC offset
utc_offset = timezone.utcoffset(None)
ref_datetime = dtm.datetime(1970, 1, 1, 12)
ref_t, time_of_day = _datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
# first test that naive time is assumed to be utc:
assert helpers.to_float_timestamp(time_of_day, ref_t) == pytest.approx(ref_t)
# test that by setting the timezone the timestamp changes accordingly:
assert (helpers.to_float_timestamp(time_of_day.replace(tzinfo=timezone), ref_t)
== pytest.approx(ref_t + (-utc_offset.total_seconds() % (24 * 60 * 60))))
@pytest.mark.parametrize('time_spec', RELATIVE_TIME_SPECS, ids=str)
def test_to_float_timestamp_default_reference(self, time_spec):
"""The reference timestamp for relative time specifications should default to now"""
now = time.time()
assert (helpers.to_float_timestamp(time_spec)
== pytest.approx(helpers.to_float_timestamp(time_spec, reference_timestamp=now)))
@pytest.mark.parametrize('time_spec', TIME_SPECS, ids=str)
def test_to_timestamp(self, time_spec):
# delegate tests to `to_float_timestamp`
assert helpers.to_timestamp(time_spec) == int(helpers.to_float_timestamp(time_spec))
def test_to_timestamp_none(self):
# this 'convenience' behaviour has been left left for backwards compatibility
assert helpers.to_timestamp(None) is None
def test_from_timestamp(self):
assert helpers.from_timestamp(1573431976) == dtm.datetime(2019, 11, 11, 0, 26, 16)
def test_create_deep_linked_url(self):
username = 'JamesTheMock'
@@ -63,7 +147,6 @@ class TestHelpers(object):
helpers.create_deep_linked_url("abc", None)
def test_effective_message_type(self):
def build_test_message(**kwargs):
config = dict(
message_id=1,
+75 -20
View File
@@ -16,7 +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 datetime
import datetime as dtm
import os
import sys
import time
@@ -28,6 +28,7 @@ from flaky import flaky
from telegram.ext import JobQueue, Updater, Job, CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import _UtcOffsetTimezone
@pytest.fixture(scope='function')
@@ -40,6 +41,8 @@ def job_queue(bot, _dp):
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason="On Appveyor precise timings are not accurate.")
@pytest.mark.skipif(os.getenv('GITHUB_ACTIONS', False) and os.name == 'nt',
reason="On windows precise timings are not accurate.")
@flaky(10, 1) # Timings aren't quite perfect
class TestJobQueue(object):
result = 0
@@ -81,6 +84,24 @@ class TestJobQueue(object):
sleep(0.02)
assert self.result == 1
def test_run_once_timezone(self, job_queue, timezone):
"""Test the correct handling of aware datetimes.
Set the target datetime to utcnow + x hours (naive) with the timezone set to utc + x hours,
which is equivalent to now.
"""
# we're parametrizing this with two different UTC offsets to exclude the possibility
# of an xpass when the test is run in a timezone with the same UTC offset
when = (dtm.datetime.utcnow() + timezone.utcoffset(None)).replace(tzinfo=timezone)
job_queue.run_once(self.job_run_once, when)
sleep(0.001)
assert self.result == 1
def test_run_once_no_time_spec(self, job_queue):
# test that an appropiate exception is raised if a job is attempted to be scheduled
# without specifying a time
with pytest.raises(ValueError):
job_queue.run_once(self.job_run_once, when=None)
def test_job_with_context(self, job_queue):
job_queue.run_once(self.job_run_once_with_context, 0.01, context=5)
sleep(0.02)
@@ -98,6 +119,13 @@ class TestJobQueue(object):
sleep(0.07)
assert self.result == 1
def test_run_repeating_first_timezone(self, job_queue, timezone):
"""Test correct scheduling of job when passing a timezone-aware datetime as ``first``"""
first = (dtm.datetime.utcnow() + timezone.utcoffset(None)).replace(tzinfo=timezone)
job_queue.run_repeating(self.job_run_once, 0.05, first=first)
sleep(0.001)
assert self.result == 1
def test_multiple(self, job_queue):
job_queue.run_once(self.job_run_once, 0.01)
job_queue.run_once(self.job_run_once, 0.02)
@@ -181,7 +209,7 @@ class TestJobQueue(object):
def test_time_unit_dt_timedelta(self, job_queue):
# Testing seconds, minutes and hours as datetime.timedelta object
# This is sufficient to test that it actually works.
interval = datetime.timedelta(seconds=0.05)
interval = dtm.timedelta(seconds=0.05)
expected_time = time.time() + interval.total_seconds()
job_queue.run_once(self.job_datetime_tests, interval)
@@ -190,43 +218,70 @@ class TestJobQueue(object):
def test_time_unit_dt_datetime(self, job_queue):
# Testing running at a specific datetime
delta = datetime.timedelta(seconds=0.05)
when = datetime.datetime.now() + delta
expected_time = time.time() + delta.total_seconds()
delta, now = dtm.timedelta(seconds=0.05), time.time()
when = dtm.datetime.utcfromtimestamp(now) + delta
expected_time = now + delta.total_seconds()
job_queue.run_once(self.job_datetime_tests, when)
sleep(0.06)
assert pytest.approx(self.job_time) == expected_time
assert self.job_time == pytest.approx(expected_time)
def test_time_unit_dt_time_today(self, job_queue):
# Testing running at a specific time today
delta = 0.05
when = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
expected_time = time.time() + delta
delta, now = 0.05, time.time()
when = (dtm.datetime.utcfromtimestamp(now) + dtm.timedelta(seconds=delta)).time()
expected_time = now + delta
job_queue.run_once(self.job_datetime_tests, when)
sleep(0.06)
assert pytest.approx(self.job_time) == expected_time
assert self.job_time == pytest.approx(expected_time)
def test_time_unit_dt_time_tomorrow(self, job_queue):
# Testing running at a specific time that has passed today. Since we can't wait a day, we
# test if the jobs next_t has been calculated correctly
delta = -2
when = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
expected_time = time.time() + delta + 60 * 60 * 24
# test if the job's next scheduled execution time has been calculated correctly
delta, now = -2, time.time()
when = (dtm.datetime.utcfromtimestamp(now) + dtm.timedelta(seconds=delta)).time()
expected_time = now + delta + 60 * 60 * 24
job_queue.run_once(self.job_datetime_tests, when)
assert pytest.approx(job_queue._queue.get(False)[0]) == expected_time
assert job_queue._queue.get(False)[0] == pytest.approx(expected_time)
def test_run_daily(self, job_queue):
delta = 0.5
time_of_day = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
expected_time = time.time() + 60 * 60 * 24 + delta
delta, now = 0.1, time.time()
time_of_day = (dtm.datetime.utcfromtimestamp(now) + dtm.timedelta(seconds=delta)).time()
expected_reschedule_time = now + delta + 24 * 60 * 60
job_queue.run_daily(self.job_run_once, time_of_day)
sleep(0.6)
sleep(0.2)
assert self.result == 1
assert pytest.approx(job_queue._queue.get(False)[0]) == expected_time
assert job_queue._queue.get(False)[0] == pytest.approx(expected_reschedule_time)
def test_run_daily_with_timezone(self, job_queue):
"""test that the weekday is retrieved based on the job's timezone
We set a job to run at the current UTC time of day (plus a small delay buffer) with a
timezone that is---approximately (see below)---UTC +24, and set it to run on the weekday
after the current UTC weekday. The job should therefore be executed now (because in UTC+24,
the time of day is the same as the current weekday is the one after the current UTC
weekday).
"""
now = time.time()
utcnow = dtm.datetime.utcfromtimestamp(now)
delta = 0.1
# must subtract one minute because the UTC offset has to be strictly less than 24h
# thus this test will xpass if run in the interval [00:00, 00:01) UTC time
# (because target time will be 23:59 UTC, so local and target weekday will be the same)
target_tzinfo = _UtcOffsetTimezone(dtm.timedelta(days=1, minutes=-1))
target_datetime = (utcnow + dtm.timedelta(days=1, minutes=-1, seconds=delta)).replace(
tzinfo=target_tzinfo)
target_time = target_datetime.timetz()
target_weekday = target_datetime.date().weekday()
expected_reschedule_time = now + delta + 24 * 60 * 60
job_queue.run_daily(self.job_run_once, time=target_time, days=(target_weekday,))
sleep(delta + 0.1)
assert self.result == 1
assert job_queue._queue.get(False)[0] == pytest.approx(expected_reschedule_time)
def test_warnings(self, job_queue):
j = Job(self.job_run_once, repeat=False)
+25 -11
View File
@@ -35,12 +35,12 @@ def message(bot):
@pytest.fixture(scope='function',
params=[
{'forward_from': User(99, 'forward_user', False),
'forward_date': datetime.now()},
'forward_date': datetime.utcnow()},
{'forward_from_chat': Chat(-23, 'channel'),
'forward_from_message_id': 101,
'forward_date': datetime.now()},
'forward_date': datetime.utcnow()},
{'reply_to_message': Message(50, None, None, None)},
{'edit_date': datetime.now()},
{'edit_date': datetime.utcnow()},
{'text': 'a text message',
'enitites': [MessageEntity('bold', 10, 4),
MessageEntity('italic', 16, 7)]},
@@ -114,7 +114,7 @@ def message_params(bot, request):
class TestMessage(object):
id = 1
from_user = User(2, 'testuser', False)
date = datetime.now()
date = datetime.utcnow()
chat = Chat(3, 'private')
test_entities = [{'length': 4, 'offset': 10, 'type': 'bold'},
{'length': 7, 'offset': 16, 'type': 'italic'},
@@ -303,16 +303,30 @@ class TestMessage(object):
def test_chat_id(self, message):
assert message.chat_id == message.chat.id
def test_link(self, message):
assert message.link is None
@pytest.mark.parametrize('type', argvalues=[Chat.SUPERGROUP, Chat.CHANNEL])
def test_link_with_username(self, message, type):
message.chat.username = 'username'
message.chat.type = 'supergroup'
message.chat.type = type
assert message.link == 'https://t.me/{}/{}'.format(message.chat.username,
message.message_id)
message.chat.type = 'channel'
assert message.link == 'https://t.me/{}/{}'.format(message.chat.username,
message.message_id)
message.chat.type = 'private'
@pytest.mark.parametrize('type, id', argvalues=[
(Chat.CHANNEL, -1003), (Chat.SUPERGROUP, -1003), (Chat.GROUP, -3)
])
def test_link_with_id(self, message, type, id):
message.chat.username = None
message.chat.id = id
message.chat.type = type
# The leading - for group ids/ -100 for supergroup ids isn't supposed to be in the link
assert message.link == 'https://t.me/c/{}/{}'.format(3, message.message_id)
@pytest.mark.parametrize('id, username', argvalues=[
(None, 'username'), (-3, None)
])
def test_link_private_chats(self, message, id, username):
message.chat.type = Chat.PRIVATE
message.chat.id = id
message.chat.username = username
assert message.link is None
def test_effective_attachment(self, message_params):
+2
View File
@@ -26,6 +26,8 @@ import telegram.ext.messagequeue as mq
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason="On Appveyor precise timings are not accurate.")
@pytest.mark.skipif(os.getenv('GITHUB_ACTIONS', False) and os.name == 'nt',
reason="On windows precise timings are not accurate.")
class TestDelayQueue(object):
N = 128
burst_limit = 30
+2 -9
View File
@@ -17,8 +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/].
import os
import sys
from platform import python_implementation
import pytest
@@ -30,17 +28,12 @@ def call_pre_commit_hook(hook_id):
@pytest.mark.nocoverage
@pytest.mark.parametrize('hook_id', argvalues=('yapf', 'flake8', 'pylint'))
@pytest.mark.skipif(not (os.getenv('TRAVIS') or os.getenv('APPVEYOR')), reason='Not running in CI')
@pytest.mark.skipif(not sys.version_info[:2] == (3, 6) or python_implementation() != 'CPython',
reason='Only running pre-commit-hooks on newest tested python version, '
'as they are slow and consistent across platforms.')
@pytest.mark.skipif(not os.getenv('TEST_PRE_COMMIT', False), reason='TEST_PRE_COMMIT not enabled')
def test_pre_commit_hook(hook_id):
assert call_pre_commit_hook(hook_id) == 0 # pragma: no cover
@pytest.mark.nocoverage
@pytest.mark.skipif(
not sys.version_info[:2] in ((3, 6), (2, 7)) or python_implementation() != 'CPython',
reason='Only testing build on 2.7 and 3.6')
@pytest.mark.skipif(not os.getenv('TEST_BUILD', False), reason='TEST_BUILD not enabled')
def test_build():
assert os.system('python setup.py bdist_dumb') == 0 # pragma: no cover