Compare commits

..

14 Commits

Author SHA1 Message Date
Hinrich Mahler 186fd1b418 Bump version to v12.7 2020-05-02 12:14:38 +02:00
Hinrich Mahler 284786fdb8 Fix doc string of run_monthly 2020-05-02 12:14:01 +02:00
Bibo-Joshi c7c56ad24e Api 4.8 (#1917)
* API 4.8

* Elaborate docs

* Address review

* Fix Message.to_json/dict() test

* More coverage

* Update telegram/bot.py

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>
2020-05-02 11:56:52 +02:00
D David Livingston ae17ce977e Add JobQueue.run_monthly() (#1705)
* added monthly job

* removed fold argument

* addressed pr comments

* addressed pr comments

* made changes from pr review

* updated comments

* clean up code

* Update .pre-commit-config.yaml

* Minor cleanup

* Update according to #1685, minor robustness changes

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-05-02 08:59:50 +02:00
Bibo-Joshi 7e231183c4 Add tzinfo kwarg to from_timestamp() (#1621)
* Add tz kwarg to from_timestamp()

* Correct handling of tzinfo=None

* Small Improvements

* None-tz yields naive dto

* Remove legacey compatibility of UTC stuff

* Update telegram/utils/helpers.py

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>
2020-05-01 22:55:13 +02:00
Bibo-Joshi 8427346a0d Add supegroup for each test bot (#1919) 2020-05-01 21:29:18 +03:00
Bibo-Joshi 632b989d90 Use @abstractmethod instead of raising NotImplementedError (#1905) 2020-05-01 21:27:34 +03:00
Bibo-Joshi 76567ba635 Stabilize CI (#1931) 2020-05-01 13:27:46 +02:00
Bibo-Joshi 2bd3f2a65a Render Notes correctly (#1914)
* Renders Notes in JobQueues docs correctly

* Notes: -> Note:
2020-04-25 12:34:13 +02:00
Bibo-Joshi 26a5006bf1 Update question template (#1910)
* Update formulation in question template

* grammar
2020-04-20 18:11:48 +02:00
Andrej730 110e2df443 Job.next_t (#1685)
* next_t property is added to Job class

Added new property to Job class - next_t, it will show the datetime when the job will be executed next time.
The property is updated during JobQueue._put method, right after job is added to queue.
Related to #1676

* Fixed newline and trailing whitespace

* Fixed PR issues, added test

1. Added setter for next_t - now JobQueue doesn't access protected Job._next_t.
2. Fixed Job class docstring.
3. Added test for next_t property.
4. Set next_t to None for run_once jobs that already ran.

* Fixed Flake8 issues

* Added next_t setter for datetime, added test

1. next_t setter now can accept datetime type.
2. added test for setting datetime to next_t and added some asserts that check tests results.
3. Also noticed Job.days setter raises ValueError when it's more appropriate to raise TypeError.

* Fixed test_warnings, added Number type to next_t setter

1. Changed type of error raised by interval setter from ValueError to TypeError..
2. Fixed test_warning after changing type of errors in Job.days and Job.interval.
3. Added Number type to next_t setter - now it can accept int too.

* Python 2 compatibility for test_job_next_t_property

Added _UTC and _UtcOffsetTimezone for python 2 compatibility

* Fixed PR issues

1. Replaced "datetime.replace tzinfo" with "datetime.astimezone"
2. Moved testing next_t setter to separate test.
3. Changed test_job_next_t_setter so it now uses non UTC timezone.

* Defining tzinfo from run_once, run_repeating

1. Added option to define Job.tzinfo from run_once (by when.tzinfo) and run_repeating (first.tzinfo)
2. Added test to check that tzinfo is always passed correctly.

* address review

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-04-18 15:08:16 +02:00
Bibo-Joshi 57546795c5 Notes on Filters.text accepting command messages (#1902) 2020-04-18 12:16:14 +02:00
Hinrich Mahler 314f87ec44 Bump version to v12.6.1 2020-04-11 09:53:29 +02:00
Bibo-Joshi 4bbcd51ef5 Fix serialization of reply_markups (#1889) 2020-04-11 09:44:40 +02:00
34 changed files with 967 additions and 185 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ 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 :)
To make it easier for us to help you, please try to follow the template below as closely as possible.
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.
-->
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
exit ${global_exit}
env:
JOB_INDEX: ${{ strategy.job-index }}
BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ==
BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzkwOTgzOTk3IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0OTY5MTc3NTAiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzMzODcxNDYxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzNjM5MzI1NzMiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDA3ODM2NjA1IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEyOTMwNzkxNjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ==
TEST_BUILD: ${{ matrix.test-build }}
TEST_PRE_COMMIT: ${{ matrix.test-pre-commit }}
shell: bash --noprofile --norc {0}
+2
View File
@@ -18,6 +18,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Alateas <https://github.com/alateas>`_
- `Ales Dokshanin <https://github.com/alesdokshanin>`_
- `Ambro17 <https://github.com/Ambro17>`_
- `Andrej Zhilenkov <https://github.com/Andrej730>`_
- `Anton Tagunov <https://github.com/anton-tagunov>`_
- `Avanatiker <https://github.com/Avanatiker>`_
- `Balduro <https://github.com/Balduro>`_
@@ -26,6 +27,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `d-qoi <https://github.com/d-qoi>`_
- `daimajia <https://github.com/daimajia>`_
- `Daniel Reed <https://github.com/nmlorg>`_
- `D David Livingston <https://github.com/daviddl9>`_
- `Eana Hufwe <https://github.com/blueset>`_
- `Ehsan Online <https://github.com/ehsanonline>`_
- `Eli Gao <https://github.com/eligao>`_
+41
View File
@@ -2,6 +2,47 @@
Changelog
=========
Version 12.7
============
*Released 2020-05-02*
**Major Changes:**
- Bot API 4.8 support. **Note:** The ``Dice`` object now has a second positional argument ``emoji``. This is relevant, if you instantiate ``Dice`` objects manually. (`#1917`_)
**New Features:**
- New method ``run_mothly`` for the ``JobQueue`` (`#1705`_)
- ``Job.next_t`` now gives the datetime of the jobs next execution (`#1685`_)
**Minor changes, CI improvements, doc fixes or bug fixes:**
- Added ``tzinfo`` argument to ``helpers.from_timestamp`` (`#1621`_)
- Stabalize CI (`#1919`_, `#1931`_)
- Use ABCs ``@abstractmethod`` instead of raising ``NotImplementedError`` for ``Handler``, ``BasePersistence`` and ``BaseFilter`` (`#1905`_)
- Doc fixes (`#1914`_, `#1902`_, `#1910`_)
.. _`#1902`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1902
.. _`#1685`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1685
.. _`#1910`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1910
.. _`#1914`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1914
.. _`#1931`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1931
.. _`#1905`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1905
.. _`#1919`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1919
.. _`#1621`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1621
.. _`#1705`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1705
.. _`#1917`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1917
Version 12.6.1
==============
*Released 2020-04-11*
**Bug fixes:**
- Fix serialization of ``reply_markup`` in media messages (`#1889`_)
.. _`#1889`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1889
Version 12.6
============
*Released 2020-04-10*
+1 -1
View File
@@ -93,7 +93,7 @@ make the development of bots easy and straightforward. These classes are contain
Telegram API support
====================
All types and methods of the Telegram Bot API **4.7** are supported.
All types and methods of the Telegram Bot API **4.8** are supported.
==========
Installing
+2 -2
View File
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '12.6' # telegram.__version__[:3]
version = '12.7' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '12.6' # telegram.__version__
release = '12.7' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
-3
View File
@@ -23,13 +23,10 @@ try:
except ImportError:
import json
from abc import ABCMeta
class TelegramObject(object):
"""Base class for most telegram objects."""
__metaclass__ = ABCMeta
_id_attrs = ()
def __str__(self):
+49 -3
View File
@@ -163,7 +163,9 @@ class Bot(TelegramObject):
if reply_markup is not None:
if isinstance(reply_markup, ReplyMarkup):
data['reply_markup'] = reply_markup.to_dict()
# We need to_json() instead of to_dict() here, because reply_markups may be
# attached to media messages, which aren't json dumped by utils.request
data['reply_markup'] = reply_markup.to_json()
else:
data['reply_markup'] = reply_markup
@@ -2101,7 +2103,7 @@ class Bot(TelegramObject):
updates may be received for a short period of time.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Notes:
Note:
1. This method will not work if an outgoing webhook is set up.
2. In order to avoid getting duplicate updates, recalculate offset after each
server response.
@@ -3404,6 +3406,8 @@ class Bot(TelegramObject):
if contains_masks is not None:
data['contains_masks'] = contains_masks
if mask_position is not None:
# We need to_json() instead of to_dict() here, because we're sending a media
# message here, which isn't json dumped by utils.request
data['mask_position'] = mask_position.to_json()
data.update(kwargs)
@@ -3472,6 +3476,8 @@ class Bot(TelegramObject):
if tgs_sticker is not None:
data['tgs_sticker'] = tgs_sticker
if mask_position is not None:
# We need to_json() instead of to_dict() here, because we're sending a media
# message here, which isn't json dumped by utils.request
data['mask_position'] = mask_position.to_json()
data.update(kwargs)
@@ -3627,6 +3633,10 @@ class Bot(TelegramObject):
reply_to_message_id=None,
reply_markup=None,
timeout=None,
explanation=None,
explanation_parse_mode=DEFAULT_NONE,
open_period=None,
close_date=None,
**kwargs):
"""
Use this method to send a native poll.
@@ -3644,6 +3654,18 @@ class Bot(TelegramObject):
answers, ignored for polls in quiz mode, defaults to False.
correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer
option, required for polls in quiz mode.
explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect
answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most
2 line feeds after entities parsing.
explanation_parse_mode (:obj:`str`, optional): Mode for parsing entities in the
explanation. See the constants in :class:`telegram.ParseMode` for the available
modes.
open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active
after creation, 5-600. Can't be used together with :attr:`close_date`.
close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix
timestamp) when the poll will be automatically closed. Must be at least 5 and no
more than 600 seconds in the future. Can't be used together with
:attr:`open_period`.
is_closed (:obj:`bool`, optional): Pass True, if the poll needs to be immediately
closed. This can be useful for poll preview.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
@@ -3673,6 +3695,12 @@ class Bot(TelegramObject):
'options': options
}
if explanation_parse_mode == DEFAULT_NONE:
if self.defaults:
explanation_parse_mode = self.defaults.parse_mode
else:
explanation_parse_mode = None
if not is_anonymous:
data['is_anonymous'] = is_anonymous
if type:
@@ -3683,6 +3711,16 @@ class Bot(TelegramObject):
data['correct_option_id'] = correct_option_id
if is_closed:
data['is_closed'] = is_closed
if explanation:
data['explanation'] = explanation
if explanation_parse_mode:
data['explanation_parse_mode'] = explanation_parse_mode
if open_period:
data['open_period'] = open_period
if close_date:
if isinstance(close_date, datetime):
close_date = to_timestamp(close_date)
data['close_date'] = close_date
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
@@ -3726,7 +3764,9 @@ class Bot(TelegramObject):
if reply_markup:
if isinstance(reply_markup, ReplyMarkup):
data['reply_markup'] = reply_markup.to_dict()
# We need to_json() instead of to_dict() here, because reply_markups may be
# attached to media messages, which aren't json dumped by utils.request
data['reply_markup'] = reply_markup.to_json()
else:
data['reply_markup'] = reply_markup
@@ -3741,6 +3781,7 @@ class Bot(TelegramObject):
reply_to_message_id=None,
reply_markup=None,
timeout=None,
emoji=None,
**kwargs):
"""
Use this method to send a dice, which will have a random value from 1 to 6. On success, the
@@ -3748,6 +3789,8 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat.
emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based.
Currently, must be one of “🎲” or “🎯”. Defaults to “🎲”
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
@@ -3773,6 +3816,9 @@ class Bot(TelegramObject):
'chat_id': chat_id,
}
if emoji:
data['emoji'] = emoji
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
+20 -3
View File
@@ -23,17 +23,26 @@ from telegram import TelegramObject
class Dice(TelegramObject):
"""
This object represents a dice with random value from 1 to 6. (The singular form of "dice" is
"die". However, PTB mimics the Telegram API, which uses the term "dice".)
This object represents a dice with random value from 1 to 6 for currently supported base eomji.
(The singular form of "dice" is "die". However, PTB mimics the Telegram API, which uses the
term "dice".)
Note:
If :attr:`emoji` is "🎯", a value of 6 currently represents a bullseye, while a value of 1
indicates that the dartboard was missed. However, this behaviour is undocumented and might
be changed by Telegram.
Attributes:
value (:obj:`int`): Value of the dice.
emoji (:obj:`str`): Emoji on which the dice throw animation is based.
Args:
value (:obj:`int`): Value of the dice, 1-6.
emoji (:obj:`str`): Emoji on which the dice throw animation is based.
"""
def __init__(self, value, **kwargs):
def __init__(self, value, emoji, **kwargs):
self.value = value
self.emoji = emoji
@classmethod
def de_json(cls, data, bot):
@@ -41,3 +50,11 @@ class Dice(TelegramObject):
return None
return cls(**data)
DICE = '🎲'
""":obj:`str`: '🎲'"""
DARTS = '🎯'
""":obj:`str`: '🎯'"""
ALL_EMOJI = [DICE, DARTS]
"""List[:obj:`str`]: List of all supported base emoji. Currently :attr:`DICE` and
:attr:`DARTS`."""
+11 -9
View File
@@ -18,8 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the BasePersistence class."""
from abc import ABC, abstractmethod
class BasePersistence(object):
class BasePersistence(ABC):
"""Interface class for adding persistence to your bot.
Subclass this object for different implementations of a persistent bot.
@@ -57,6 +59,7 @@ class BasePersistence(object):
self.store_chat_data = store_chat_data
self.store_bot_data = store_bot_data
@abstractmethod
def get_user_data(self):
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the user_data if stored, or an empty
@@ -65,8 +68,8 @@ class BasePersistence(object):
Returns:
:obj:`defaultdict`: The restored user data.
"""
raise NotImplementedError
@abstractmethod
def get_chat_data(self):
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the chat_data if stored, or an empty
@@ -75,8 +78,8 @@ class BasePersistence(object):
Returns:
:obj:`defaultdict`: The restored chat data.
"""
raise NotImplementedError
@abstractmethod
def get_bot_data(self):
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the bot_data if stored, or an empty
@@ -85,8 +88,8 @@ class BasePersistence(object):
Returns:
:obj:`defaultdict`: The restored bot data.
"""
raise NotImplementedError
@abstractmethod
def get_conversations(self, name):
""""Will be called by :class:`telegram.ext.Dispatcher` when a
:class:`telegram.ext.ConversationHandler` is added if
@@ -99,8 +102,8 @@ class BasePersistence(object):
Returns:
:obj:`dict`: The restored conversations for the handler.
"""
raise NotImplementedError
@abstractmethod
def update_conversation(self, name, key, new_state):
"""Will be called when a :attr:`telegram.ext.ConversationHandler.update_state`
is called. this allows the storeage of the new state in the persistence.
@@ -110,8 +113,8 @@ class BasePersistence(object):
key (:obj:`tuple`): The key the state is changed for.
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
"""
raise NotImplementedError
@abstractmethod
def update_user_data(self, user_id, data):
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
@@ -120,8 +123,8 @@ class BasePersistence(object):
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id].
"""
raise NotImplementedError
@abstractmethod
def update_chat_data(self, chat_id, data):
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
@@ -130,8 +133,8 @@ class BasePersistence(object):
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id].
"""
raise NotImplementedError
@abstractmethod
def update_bot_data(self, data):
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
@@ -139,7 +142,6 @@ class BasePersistence(object):
Args:
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data` .
"""
raise NotImplementedError
def flush(self):
"""Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the
+55 -28
View File
@@ -21,13 +21,14 @@
import re
from future.utils import string_types
from abc import ABC, abstractmethod
from telegram import Chat, Update, MessageEntity
__all__ = ['Filters', 'BaseFilter', 'InvertedFilter', 'MergedFilter']
class BaseFilter(object):
class BaseFilter(ABC):
"""Base class for all Message Filters.
Subclassing from this class filters to be combined using bitwise operators:
@@ -103,6 +104,7 @@ class BaseFilter(object):
self.name = self.__class__.__name__
return self.name
@abstractmethod
def filter(self, update):
"""This method must be overwritten.
@@ -118,8 +120,6 @@ class BaseFilter(object):
"""
raise NotImplementedError
class InvertedFilter(BaseFilter):
"""Represents a filter that has been inverted.
@@ -215,6 +215,38 @@ class MergedFilter(BaseFilter):
self.and_filter or self.or_filter)
class _DiceEmoji(BaseFilter):
def __init__(self, emoji=None, name=None):
self.name = 'Filters.dice.{}'.format(name) if name else 'Filters.dice'
self.emoji = emoji
class _DiceValues(BaseFilter):
def __init__(self, values, name, emoji=None):
self.values = [values] if isinstance(values, int) else values
self.emoji = emoji
self.name = '{}({})'.format(name, values)
def filter(self, message):
if bool(message.dice and message.dice.value in self.values):
if self.emoji:
return message.dice.emoji == self.emoji
return True
def __call__(self, update):
if isinstance(update, Update):
return self.filter(update.effective_message)
else:
return self._DiceValues(update, self.name, emoji=self.emoji)
def filter(self, message):
if bool(message.dice):
if self.emoji:
return message.dice.emoji == self.emoji
return True
class Filters(object):
"""Predefined filters for use as the `filter` argument of :class:`telegram.ext.MessageHandler`.
@@ -273,8 +305,11 @@ class Filters(object):
MessageHandler(Filters.text(buttons), callback_method)
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
* Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
* Messages containing a command are accepted by this filter. Use
``Filters.text & (~Filters.command)``, if you want to filter only text messages without
commands.
Args:
update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only
@@ -350,6 +385,9 @@ class Filters(object):
MessageHandler(Filters.command, command_at_start_callback)
MessageHandler(Filters.command(False), command_anywhere_callback)
Note:
``Filters.text`` also accepts messages containing a command.
Args:
update (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot
command. Defaults to ``True``.
@@ -961,26 +999,9 @@ officedocument.wordprocessingml.document")``-
poll = _Poll()
"""Messages that contain a :class:`telegram.Poll`."""
class _Dice(BaseFilter):
name = 'Filters.dice'
class _DiceValues(BaseFilter):
def __init__(self, values):
self.values = [values] if isinstance(values, int) else values
self.name = 'Filters.dice({})'.format(values)
def filter(self, message):
return bool(message.dice and message.dice.value in self.values)
def __call__(self, update):
if isinstance(update, Update):
return self.filter(update.effective_message)
else:
return self._DiceValues(update)
def filter(self, message):
return bool(message.dice)
class _Dice(_DiceEmoji):
dice = _DiceEmoji('🎲', 'dice')
darts = _DiceEmoji('🎯', 'darts')
dice = _Dice()
"""Dice Messages. If an integer or a list of integers is passed, it filters messages to only
@@ -998,9 +1019,15 @@ officedocument.wordprocessingml.document")``-
update (:obj:`int` | List[:obj:`int`], optional): Which values to allow. If not
specified, will allow any dice message.
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
``Filters.text | Filters.dice``.
Attributes:
dice: Dice messages with the emoji 🎲. Passing a list of integers is supported just as for
:attr:`Filters.dice`.
darts: Dice messages with the emoji 🎯. Passing a list of integers is supported just as for
:attr:`Filters.dice`.
"""
class language(BaseFilter):
+4 -2
View File
@@ -18,8 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the base class for handlers as used by the Dispatcher."""
from abc import ABC, abstractmethod
class Handler(object):
class Handler(ABC):
"""The base class for all update handlers. Create custom handlers by inheriting from it.
Attributes:
@@ -82,6 +84,7 @@ class Handler(object):
self.pass_user_data = pass_user_data
self.pass_chat_data = pass_chat_data
@abstractmethod
def check_update(self, update):
"""
This method is called to determine if an update should be handled by
@@ -96,7 +99,6 @@ class Handler(object):
when the update gets handled.
"""
raise NotImplementedError
def handle_update(self, update, dispatcher, check_result, context=None):
"""
+177 -11
View File
@@ -18,6 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes JobQueue and Job."""
import calendar
import datetime
import logging
import time
@@ -29,7 +30,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
from telegram.utils.helpers import to_float_timestamp
class Days(object):
@@ -104,6 +105,7 @@ class JobQueue(object):
# enqueue:
self.logger.debug('Putting job %s with t=%s', job.name, time_spec)
self._queue.put((next_t, job))
job._set_next_t(next_t)
# Wake up the loop if this job should be executed next
self._set_next_peek(next_t)
@@ -135,6 +137,9 @@ class JobQueue(object):
job should run. This could be either today or, if the time has already passed,
tomorrow. If the timezone (``time.tzinfo``) is ``None``, UTC will be assumed.
If ``when`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
then ``when.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
name (:obj:`str`, optional): The name of the new job. Defaults to
@@ -145,7 +150,14 @@ class JobQueue(object):
queue.
"""
job = Job(callback, repeat=False, context=context, name=name, job_queue=self)
tzinfo = when.tzinfo if isinstance(when, (datetime.datetime, datetime.time)) else None
job = Job(callback,
repeat=False,
context=context,
name=name,
job_queue=self,
tzinfo=tzinfo)
self._put(job, time_spec=when)
return job
@@ -179,6 +191,9 @@ class JobQueue(object):
job should run. This could be either today or, if the time has already passed,
tomorrow. If the timezone (``time.tzinfo``) is ``None``, UTC will be assumed.
If ``first`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
then ``first.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
Defaults to ``interval``
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
@@ -189,21 +204,131 @@ class JobQueue(object):
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
Notes:
Note:
`interval` is always respected "as-is". That means that if DST changes during that
interval, the job might not run at the time one would expect. It is always recommended
to pin servers to UTC time, then time related behaviour can always be expected.
"""
tzinfo = first.tzinfo if isinstance(first, (datetime.datetime, datetime.time)) else None
job = Job(callback,
interval=interval,
repeat=True,
context=context,
name=name,
job_queue=self)
job_queue=self,
tzinfo=tzinfo)
self._put(job, time_spec=first)
return job
def run_monthly(self, callback, when, day, context=None, name=None, day_is_strict=True):
"""Creates a new ``Job`` that runs on a monthly basis and adds it to the queue.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. Callback signature for context based API:
``def callback(CallbackContext)``
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
its ``job.context`` or change it to a repeating job.
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``when.tzinfo``) is ``None``, UTC will be assumed. This will also implicitly
define ``Job.tzinfo``.
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
be within the range of 1 and 31, inclusive.
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
day_is_strict (:obj:`bool`, optional): If ``False`` and day > month.days, will pick
the last day in the month. Defaults to ``True``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
"""
tzinfo = when.tzinfo if isinstance(when, (datetime.datetime, datetime.time)) else None
if 1 <= day <= 31:
next_dt = self._get_next_month_date(day, day_is_strict, when, allow_now=True)
job = Job(callback, repeat=False, context=context, name=name, job_queue=self,
is_monthly=True, day_is_strict=day_is_strict, tzinfo=tzinfo)
self._put(job, time_spec=next_dt)
return job
else:
raise ValueError("The elements of the 'day' argument should be from 1 up to"
" and including 31")
def _get_next_month_date(self, day, day_is_strict, when, allow_now=False):
"""This method returns the date that the next monthly job should be scheduled.
Args:
day (:obj:`int`): The day of the month the job should run.
day_is_strict (:obj:`bool`):
Specification as to whether the specified day of job should be strictly
respected. If day_is_strict is ``True`` it ignores months whereby the
specified date does not exist (e.g February 31st). If it set to ``False``,
it returns the last valid date of the month instead. For example,
if the user runs a job on the 31st of every month, and sets
the day_is_strict variable to ``False``, April, for example,
the job would run on April 30th.
when (:obj:`datetime.time`): Time of day at which the job should run. If the
timezone (``time.tzinfo``) is ``None``, UTC will be assumed.
allow_now (:obj:`bool`): Whether executing the job right now is a feasible options.
For stability reasons, this defaults to :obj:`False`, but it needs to be :obj:`True`
on initializing a job.
"""
dt = datetime.datetime.now(tz=when.tzinfo or datetime.timezone.utc)
dt_time = dt.time().replace(tzinfo=when.tzinfo)
days_in_current_month = calendar.monthrange(dt.year, dt.month)[1]
days_till_months_end = days_in_current_month - dt.day
if days_in_current_month < day:
# if the day does not exist in the current month (e.g Feb 31st)
if day_is_strict is False:
# set day as last day of month instead
next_dt = dt + datetime.timedelta(days=days_till_months_end)
else:
# else set as day in subsequent month. Subsequent month is
# guaranteed to have the date, if current month does not have the date.
next_dt = dt + datetime.timedelta(days=days_till_months_end + day)
else:
# if the day exists in the current month
if dt.day < day:
# day is upcoming
next_dt = dt + datetime.timedelta(day - dt.day)
elif dt.day > day or (dt.day == day and ((not allow_now and dt_time >= when)
or (allow_now and dt_time > when))):
# run next month if day has already passed
next_year = dt.year + 1 if dt.month == 12 else dt.year
next_month = 1 if dt.month == 12 else dt.month + 1
days_in_next_month = calendar.monthrange(next_year, next_month)[1]
next_month_has_date = days_in_next_month >= day
if next_month_has_date:
next_dt = dt + datetime.timedelta(days=days_till_months_end + day)
elif day_is_strict:
# schedule the subsequent month if day is strict
next_dt = dt + datetime.timedelta(
days=days_till_months_end + days_in_next_month + day)
else:
# schedule in the next month last date if day is not strict
next_dt = dt + datetime.timedelta(days=days_till_months_end
+ days_in_next_month)
else:
# day is today but time has not yet come
next_dt = dt
# Set the correct time
next_dt = next_dt.replace(hour=when.hour, minute=when.minute, second=when.second,
microsecond=when.microsecond)
# fold is new in Py3.6
if hasattr(next_dt, 'fold'):
next_dt = next_dt.replace(fold=when.fold)
return next_dt
def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None):
"""Creates a new ``Job`` that runs on a daily basis and adds it to the queue.
@@ -217,6 +342,7 @@ class JobQueue(object):
its ``job.context`` or change it to a repeating job.
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``time.tzinfo``) is ``None``, UTC will be assumed.
``time.tzinfo`` will implicitly define ``Job.tzinfo``.
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.
@@ -228,7 +354,7 @@ class JobQueue(object):
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
Notes:
Note:
Daily is just an alias for "24 Hours". That means that if DST changes during that
interval, the job might not run at the time one would expect. It is always recommended
to pin servers to UTC time, then time related behaviour can always be expected.
@@ -300,7 +426,13 @@ class JobQueue(object):
if job.repeat and not job.removed:
self._put(job, previous_t=t)
elif job.is_monthly and not job.removed:
dt = datetime.datetime.now(tz=job.tzinfo)
dt_time = dt.time().replace(tzinfo=job.tzinfo)
self._put(job, time_spec=self._get_next_month_date(dt.day, job.day_is_strict,
dt_time))
else:
job._set_next_t(None)
self.logger.debug('Dropping non-repeating or removed job %s', job.name)
def start(self):
@@ -367,6 +499,8 @@ class Job(object):
callback (:obj:`callable`): The callback function that should be executed by the new job.
context (:obj:`object`): Optional. Additional data needed for the callback function.
name (:obj:`str`): Optional. The name of the new job.
is_monthly (:obj: `bool`): Optional. Indicates whether it is a monthly job.
day_is_strict (:obj: `bool`): Optional. Indicates whether the monthly jobs day is strict.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new job.
@@ -393,6 +527,11 @@ class Job(object):
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.
is_monthly (:obj:`bool`, optional): If this job is supposed to be a monthly scheduled job.
Defaults to ``False``.
day_is_strict (:obj:`bool`, optional): If ``False`` and day > month.days, will pick the
last day in the month. Defaults to ``True``. Only relevant when ``is_monthly`` is
``True``.
"""
def __init__(self,
@@ -403,7 +542,9 @@ class Job(object):
days=Days.EVERY_DAY,
name=None,
job_queue=None,
tzinfo=None):
tzinfo=None,
is_monthly=False,
day_is_strict=True):
self.callback = callback
self.context = context
@@ -412,11 +553,14 @@ class Job(object):
self._repeat = None
self._interval = None
self.interval = interval
self._next_t = None
self.repeat = repeat
self.is_monthly = is_monthly
self.day_is_strict = day_is_strict
self._days = None
self.days = days
self.tzinfo = tzinfo or _UTC
self.tzinfo = tzinfo or datetime.timezone.utc
self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None
@@ -438,6 +582,7 @@ class Job(object):
"""
self._remove.set()
self._next_t = None
@property
def removed(self):
@@ -471,8 +616,8 @@ class Job(object):
raise ValueError("The 'interval' can not be 'None' when 'repeat' is set to 'True'")
if not (interval is None or isinstance(interval, (Number, datetime.timedelta))):
raise ValueError("The 'interval' must be of type 'datetime.timedelta',"
" 'int' or 'float'")
raise TypeError("The 'interval' must be of type 'datetime.timedelta',"
" 'int' or 'float'")
self._interval = interval
@@ -485,6 +630,27 @@ class Job(object):
else:
return interval
@property
def next_t(self):
"""
:obj:`datetime.datetime`: Datetime for the next job execution.
Datetime is localized according to :attr:`tzinfo`.
If job is removed or already ran it equals to ``None``.
"""
return datetime.datetime.fromtimestamp(self._next_t, self.tzinfo) if self._next_t else None
def _set_next_t(self, next_t):
if isinstance(next_t, datetime.datetime):
# Set timezone to UTC in case datetime is in local timezone.
next_t = next_t.astimezone(datetime.timezone.utc)
next_t = to_float_timestamp(next_t)
elif not (isinstance(next_t, Number) or next_t is None):
raise TypeError("The 'next_t' argument should be one of the following types: "
"'float', 'int', 'datetime.datetime' or 'NoneType'")
self._next_t = next_t
@property
def repeat(self):
""":obj:`bool`: Optional. If this job should periodically execute its callback function."""
@@ -504,10 +670,10 @@ class Job(object):
@days.setter
def days(self, days):
if not isinstance(days, tuple):
raise ValueError("The 'days' argument should be of type 'tuple'")
raise TypeError("The 'days' argument should be of type 'tuple'")
if not all(isinstance(day, int) for day in days):
raise ValueError("The elements of the 'days' argument should be of type 'int'")
raise TypeError("The elements of the 'days' argument should be of type 'int'")
if not all(0 <= day <= 6 for day in days):
raise ValueError("The elements of the 'days' argument should be from 0 up to and "
+1 -1
View File
@@ -249,7 +249,7 @@ class MessageQueue(object):
``DelayQueue`` (if set to ``False``), resulting in needed delays to avoid
hitting specified limits. Defaults to ``False``.
Notes:
Note:
Method is designed to accept ``telegram.utils.promise.Promise`` as ``promise``
argument, but other callables could be used too. For example, lambdas or simple
functions could be used to wrap original func to be called with needed args. In that
+1 -1
View File
@@ -195,7 +195,7 @@ class MaskPosition(TelegramObject):
size, from top to bottom.
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
Notes:
Note:
:attr:`type` should be one of the following: `forehead`, `eyes`, `mouth` or `chin`. You can
use the classconstants for those.
+98 -3
View File
@@ -19,7 +19,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Poll."""
from telegram import (TelegramObject, User)
import sys
from telegram import (TelegramObject, User, MessageEntity)
from telegram.utils.helpers import to_timestamp, from_timestamp
class PollOption(TelegramObject):
@@ -95,6 +98,14 @@ class Poll(TelegramObject):
type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`.
allows_multiple_answers (:obj:`bool`): True, if the poll allows multiple answers.
correct_option_id (:obj:`int`): Optional. Identifier of the correct answer option.
explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect
answer or taps on the lamp icon in a quiz-style poll.
explanation_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities
like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`.
open_period (:obj:`int`): Optional. Amount of time in seconds the poll will be active
after creation.
close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be
automatically closed.
Args:
id (:obj:`str`): Unique poll identifier.
@@ -107,11 +118,32 @@ class Poll(TelegramObject):
correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer option.
Available only for polls in the quiz mode, which are closed, or was sent (not
forwarded) by the bot or to the private chat with the bot.
explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect
answer or taps on the lamp icon in a quiz-style poll, 0-200 characters.
explanation_entities (List[:class:`telegram.MessageEntity`], optional): Special entities
like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`.
open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active
after creation.
close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the
poll will be automatically closed. Converted to :obj:`datetime.datetime`.
"""
def __init__(self, id, question, options, total_voter_count, is_closed, is_anonymous, type,
allows_multiple_answers, correct_option_id=None, **kwargs):
def __init__(self,
id,
question,
options,
total_voter_count,
is_closed,
is_anonymous,
type,
allows_multiple_answers,
correct_option_id=None,
explanation=None,
explanation_entities=None,
open_period=None,
close_date=None,
**kwargs):
self.id = id
self.question = question
self.options = options
@@ -121,6 +153,10 @@ class Poll(TelegramObject):
self.type = type
self.allows_multiple_answers = allows_multiple_answers
self.correct_option_id = correct_option_id
self.explanation = explanation
self.explanation_entities = explanation_entities
self.open_period = open_period
self.close_date = close_date
self._id_attrs = (self.id,)
@@ -132,6 +168,8 @@ class Poll(TelegramObject):
data = super(Poll, cls).de_json(data, bot)
data['options'] = [PollOption.de_json(option, bot) for option in data['options']]
data['explanation_entities'] = MessageEntity.de_list(data.get('explanation_entities'), bot)
data['close_date'] = from_timestamp(data.get('close_date'))
return cls(**data)
@@ -139,9 +177,66 @@ class Poll(TelegramObject):
data = super(Poll, self).to_dict()
data['options'] = [x.to_dict() for x in self.options]
if self.explanation_entities:
data['explanation_entities'] = [e.to_dict() for e in self.explanation_entities]
data['close_date'] = to_timestamp(data.get('close_date'))
return data
def parse_explanation_entity(self, entity):
"""Returns the text from a given :class:`telegram.MessageEntity`.
Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice ``Message.text`` with the offset and length.)
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to this message.
Returns:
:obj:`str`: The text of the given entity.
"""
# Is it a narrow build, if so we don't need to convert
if sys.maxunicode == 0xffff:
return self.explanation[entity.offset:entity.offset + entity.length]
else:
entity_text = self.explanation.encode('utf-16-le')
entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2]
return entity_text.decode('utf-16-le')
def parse_explanation_entities(self, types=None):
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this polls explanation filtered by their ``type`` attribute as
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
Note:
This method should always be used instead of the :attr:`explanation_entities`
attribute, since it calculates the correct substring from the message text based on
UTF-16 codepoints. See :attr:`parse_explanation_entity` for more info.
Args:
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
``type`` attribute of an entity is contained in this list, it will be returned.
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
Returns:
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
if types is None:
types = MessageEntity.ALL_TYPES
return {
entity: self.parse_explanation_entity(entity)
for entity in self.explanation_entities if entity.type in types
}
REGULAR = "regular"
""":obj:`str`: 'regular'"""
QUIZ = "quiz"
+14 -43
View File
@@ -75,46 +75,12 @@ def escape_markdown(text, version=1, entity_type=None):
# -------- 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__ = \
def _datetime_to_float_timestamp(dt_obj):
"""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."""
If the datetime object is timezone-naive, it is assumed to be in UTC."""
if dt_obj.tzinfo is None:
dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc)
return dt_obj.timestamp()
def to_float_timestamp(t, reference_timestamp=None):
@@ -196,22 +162,27 @@ def to_timestamp(dt_obj, reference_timestamp=None):
return int(to_float_timestamp(dt_obj, reference_timestamp)) if dt_obj is not None else None
def from_timestamp(unixtime):
def from_timestamp(unixtime, tzinfo=dtm.timezone.utc):
"""
Converts an (integer) unix timestamp to a naive datetime object in UTC.
Converts an (integer) unix timestamp to a timezone aware datetime object.
``None`` s are left alone (i.e. ``from_timestamp(None)`` is ``None``).
Args:
unixtime (int): integer POSIX timestamp
tzinfo (:obj:`datetime.tzinfo`, optional): The timezone, the timestamp is to be converted
to. Defaults to UTC.
Returns:
equivalent :obj:`datetime.datetime` value in naive UTC if ``timestamp`` is not
timezone aware equivalent :obj:`datetime.datetime` value if ``timestamp`` is not
``None``; else ``None``
"""
if unixtime is None:
return None
return dtm.datetime.utcfromtimestamp(unixtime)
if tzinfo is not None:
return dtm.datetime.fromtimestamp(unixtime, tz=tzinfo)
else:
return dtm.datetime.utcfromtimestamp(unixtime)
# -------- end --------
+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.6'
__version__ = '12.7'
+19 -2
View File
@@ -21,6 +21,9 @@ import json
import base64
import os
import random
import pytest
from telegram.utils.request import Request
from telegram.error import RetryAfter
# 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
@@ -30,7 +33,7 @@ FALLBACKS = [
'token': '579694714:AAHRLL5zBVy4Blx2jRFKe1HlfnXCg08WuLY',
'payment_provider_token': '284685063:TEST:NjQ0NjZlNzI5YjJi',
'chat_id': '675666224',
'super_group_id': '-1001493296829',
'super_group_id': '-1001310911135',
'channel_id': '@pythontelegrambottests',
'bot_name': 'PTB tests fallback 1',
'bot_username': '@ptb_fallback_1_bot'
@@ -38,7 +41,7 @@ FALLBACKS = [
'token': '558194066:AAEEylntuKSLXj9odiv3TnX7Z5KY2J3zY3M',
'payment_provider_token': '284685063:TEST:YjEwODQwMTFmNDcy',
'chat_id': '675666224',
'super_group_id': '-1001493296829',
'super_group_id': '-1001221216830',
'channel_id': '@pythontelegrambottests',
'bot_name': 'PTB tests fallback 2',
'bot_username': '@ptb_fallback_2_bot'
@@ -73,3 +76,17 @@ def get(name, fallback):
def get_bot():
return {k: get(k, v) for k, v in random.choice(FALLBACKS).items()}
# Patch request to xfail on flood control errors
original_request_wrapper = Request._request_wrapper
def patient_request_wrapper(*args, **kwargs):
try:
return original_request_wrapper(*args, **kwargs)
except RetryAfter as e:
pytest.xfail('Not waiting for flood control: {}'.format(e))
Request._request_wrapper = patient_request_wrapper
+25 -2
View File
@@ -31,7 +31,7 @@ from telegram import (Bot, Message, User, Chat, MessageEntity, Update,
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery,
ChosenInlineResult)
from telegram.ext import Dispatcher, JobQueue, Updater, BaseFilter, Defaults
from telegram.utils.helpers import _UtcOffsetTimezone
from telegram.error import BadRequest
from tests.bots import get_bot
GITHUB_ACTION = os.getenv('GITHUB_ACTION', False)
@@ -280,4 +280,27 @@ def utc_offset(request):
@pytest.fixture()
def timezone(utc_offset):
return _UtcOffsetTimezone(utc_offset)
return datetime.timezone(utc_offset)
def expect_bad_request(func, message, reason):
"""
Wrapper for testing bot functions expected to result in an :class:`telegram.error.BadRequest`.
Makes it XFAIL, if the specified error message is present.
Args:
func: The callable to be executed.
message: The expected message of the bad request error. If another message is present,
the error will be reraised.
reason: Explanation for the XFAIL.
Returns:
On success, returns the return value of :attr:`func`
"""
try:
return func()
except BadRequest as e:
if message in str(e):
pytest.xfail('{}. {}'.format(reason, e))
else:
raise e
+82 -7
View File
@@ -27,9 +27,10 @@ from future.utils import string_types
from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup,
InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent,
ShippingOption, LabeledPrice, ChatPermissions, Poll, BotCommand,
InlineQueryResultDocument)
InlineQueryResultDocument, Dice, MessageEntity, ParseMode)
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
from telegram.utils.helpers import from_timestamp, escape_markdown
from tests.conftest import expect_bad_request
BASE_TIME = time.time()
HIGHSCORE_DELTA = 1450000000
@@ -213,18 +214,86 @@ class TestBot(object):
assert poll.question == question
assert poll.total_voter_count == 0
explanation = '[Here is a link](https://google.com)'
explanation_entities = [
MessageEntity(MessageEntity.TEXT_LINK, 0, 14, url='https://google.com')
]
message_quiz = bot.send_poll(chat_id=super_group_id, question=question, options=answers,
type=Poll.QUIZ, correct_option_id=2, is_closed=True)
type=Poll.QUIZ, correct_option_id=2, is_closed=True,
explanation=explanation,
explanation_parse_mode=ParseMode.MARKDOWN_V2)
assert message_quiz.poll.correct_option_id == 2
assert message_quiz.poll.type == Poll.QUIZ
assert message_quiz.poll.is_closed
assert message_quiz.poll.explanation == 'Here is a link'
assert message_quiz.poll.explanation_entities == explanation_entities
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_dice(self, bot, chat_id):
message = bot.send_dice(chat_id)
@pytest.mark.parametrize(['open_period', 'close_date'], [(5, None), (None, True)])
def test_send_open_period(self, bot, super_group_id, open_period, close_date):
question = 'Is this a test?'
answers = ['Yes', 'No', 'Maybe']
reply_markup = InlineKeyboardMarkup.from_button(
InlineKeyboardButton(text='text', callback_data='data'))
if close_date:
close_date = dtm.datetime.utcnow() + dtm.timedelta(seconds=5)
message = bot.send_poll(chat_id=super_group_id, question=question, options=answers,
is_anonymous=False, allows_multiple_answers=True, timeout=60,
open_period=open_period, close_date=close_date)
time.sleep(5.1)
new_message = bot.edit_message_reply_markup(chat_id=super_group_id,
message_id=message.message_id,
reply_markup=reply_markup, timeout=60)
assert new_message.poll.id == message.poll.id
assert new_message.poll.is_closed
@flaky(3, 1)
@pytest.mark.timeout(10)
@pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True)
def test_send_poll_default_parse_mode(self, default_bot, super_group_id):
explanation = 'Italic Bold Code'
explanation_markdown = '_Italic_ *Bold* `Code`'
question = 'Is this a test?'
answers = ['Yes', 'No', 'Maybe']
message = default_bot.send_poll(chat_id=super_group_id, question=question, options=answers,
type=Poll.QUIZ, correct_option_id=2, is_closed=True,
explanation=explanation_markdown)
assert message.poll.explanation == explanation
assert message.poll.explanation_entities == [
MessageEntity(MessageEntity.ITALIC, 0, 6),
MessageEntity(MessageEntity.BOLD, 7, 4),
MessageEntity(MessageEntity.CODE, 12, 4)
]
message = default_bot.send_poll(chat_id=super_group_id, question=question, options=answers,
type=Poll.QUIZ, correct_option_id=2, is_closed=True,
explanation=explanation_markdown,
explanation_parse_mode=None)
assert message.poll.explanation == explanation_markdown
assert message.poll.explanation_entities == []
message = default_bot.send_poll(chat_id=super_group_id, question=question, options=answers,
type=Poll.QUIZ, correct_option_id=2, is_closed=True,
explanation=explanation_markdown,
explanation_parse_mode='HTML')
assert message.poll.explanation == explanation_markdown
assert message.poll.explanation_entities == []
@flaky(3, 1)
@pytest.mark.timeout(10)
@pytest.mark.parametrize('emoji', Dice.ALL_EMOJI + [None])
def test_send_dice(self, bot, chat_id, emoji):
message = bot.send_dice(chat_id, emoji=emoji)
assert message.dice
if emoji is None:
assert message.dice.emoji == Dice.DICE
else:
assert message.dice.emoji == emoji
@flaky(3, 1)
@pytest.mark.timeout(10)
@@ -552,7 +621,7 @@ class TestBot(object):
chat = bot.get_chat(super_group_id)
assert chat.type == 'supergroup'
assert chat.title == '>>> telegram.Bot(test)'
assert chat.title == '>>> telegram.Bot(test) @{}'.format(bot.username)
assert chat.id == int(super_group_id)
# TODO: Add bot to group to test there too
@@ -816,13 +885,19 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_chat_photo(self, bot, channel_id):
with open('tests/data/telegram_test_channel.jpg', 'rb') as f:
def func():
assert bot.set_chat_photo(channel_id, f)
with open('tests/data/telegram_test_channel.jpg', 'rb') as f:
expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_delete_chat_photo(self, bot, channel_id):
assert bot.delete_chat_photo(channel_id)
def func():
assert bot.delete_chat_photo(channel_id)
expect_bad_request(func, 'Chat_not_modified', 'Chat photo was not set.')
@flaky(3, 1)
@pytest.mark.timeout(10)
+9 -2
View File
@@ -22,6 +22,7 @@ import pytest
from flaky import flaky
from telegram import ChatPhoto, Voice, TelegramError
from tests.conftest import expect_bad_request
@pytest.fixture(scope='function')
@@ -33,7 +34,10 @@ def chatphoto_file():
@pytest.fixture(scope='function')
def chat_photo(bot, super_group_id):
return bot.get_chat(super_group_id, timeout=50).photo
def func():
return bot.get_chat(super_group_id, timeout=50).photo
return expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.')
class TestChatPhoto(object):
@@ -46,7 +50,10 @@ class TestChatPhoto(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_all_args(self, bot, super_group_id, chatphoto_file, chat_photo, thumb_file):
assert bot.set_chat_photo(super_group_id, chatphoto_file)
def func():
assert bot.set_chat_photo(super_group_id, chatphoto_file)
expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.')
@flaky(3, 1)
@pytest.mark.timeout(10)
+9 -5
View File
@@ -22,19 +22,22 @@ import pytest
from telegram import Dice
@pytest.fixture(scope="class")
def dice():
return Dice(value=5)
@pytest.fixture(scope="class",
params=Dice.ALL_EMOJI)
def dice(request):
return Dice(value=5, emoji=request.param)
class TestDice(object):
value = 4
def test_de_json(self, bot):
json_dict = {'value': self.value}
@pytest.mark.parametrize('emoji', Dice.ALL_EMOJI)
def test_de_json(self, bot, emoji):
json_dict = {'value': self.value, 'emoji': emoji}
dice = Dice.de_json(json_dict, bot)
assert dice.value == self.value
assert dice.emoji == emoji
assert Dice.de_json(None, bot) is None
def test_to_dict(self, dice):
@@ -42,3 +45,4 @@ class TestDice(object):
assert isinstance(dice_dict, dict)
assert dice_dict['value'] == dice.value
assert dice_dict['emoji'] == dice.emoji
+36
View File
@@ -373,6 +373,12 @@ class TestDispatcher(object):
def update_user_data(self, user_id, data):
raise Exception
def get_conversations(self, name):
pass
def update_conversation(self, name, key, new_state):
pass
def start1(b, u):
pass
@@ -470,6 +476,21 @@ class TestDispatcher(object):
def update_user_data(self, user_id, data):
self.update(data)
def get_chat_data(self):
pass
def get_bot_data(self):
pass
def get_user_data(self):
pass
def get_conversations(self, name):
pass
def update_conversation(self, name, key, new_state):
pass
def callback(update, context):
pass
@@ -513,6 +534,21 @@ class TestDispatcher(object):
def update_user_data(self, user_id, data):
self.test_flag_user_data = True
def update_conversation(self, name, key, new_state):
pass
def get_conversations(self, name):
pass
def get_user_data(self):
pass
def get_bot_data(self):
pass
def get_chat_data(self):
pass
def callback(update, context):
pass
+21 -8
View File
@@ -622,22 +622,37 @@ class TestFilters(object):
update.message.poll = 'test'
assert Filters.poll(update)
def test_filters_dice(self, update):
update.message.dice = Dice(4)
@pytest.mark.parametrize('emoji', Dice.ALL_EMOJI)
def test_filters_dice(self, update, emoji):
update.message.dice = Dice(4, emoji)
assert Filters.dice(update)
update.message.dice = None
assert not Filters.dice(update)
def test_filters_dice_iterable(self, update):
@pytest.mark.parametrize('emoji', Dice.ALL_EMOJI)
def test_filters_dice_list(self, update, emoji):
update.message.dice = None
assert not Filters.dice(5)(update)
update.message.dice = Dice(5)
update.message.dice = Dice(5, emoji)
assert Filters.dice(5)(update)
assert Filters.dice({5, 6})(update)
assert not Filters.dice(1)(update)
assert not Filters.dice([2, 3])(update)
def test_filters_dice_type(self, update):
update.message.dice = Dice(5, '🎲')
assert Filters.dice.dice(update)
assert Filters.dice.dice([4, 5])(update)
assert not Filters.dice.darts(update)
assert not Filters.dice.dice([6])(update)
update.message.dice = Dice(5, '🎯')
assert Filters.dice.darts(update)
assert Filters.dice.darts([4, 5])(update)
assert not Filters.dice.dice(update)
assert not Filters.dice.darts([6])(update)
def test_language_filter_single(self, update):
update.message.from_user.language_code = 'en_US'
assert (Filters.language('en_US'))(update)
@@ -730,10 +745,8 @@ class TestFilters(object):
class _CustomFilter(BaseFilter):
pass
custom = _CustomFilter()
with pytest.raises(NotImplementedError):
(custom & Filters.text)(update)
with pytest.raises(TypeError, match='Can\'t instantiate abstract class _CustomFilter'):
_CustomFilter()
def test_custom_unnamed_filter(self, update):
class Unnamed(BaseFilter):
+13 -5
View File
@@ -27,14 +27,14 @@ from telegram import User
from telegram import MessageEntity
from telegram.message import Message
from telegram.utils import helpers
from telegram.utils.helpers import _UtcOffsetTimezone, _datetime_to_float_timestamp
from telegram.utils.helpers import _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))),
ABSOLUTE_TIME_SPECS = [dtm.datetime.now(tz=dtm.timezone(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))),
TIME_OF_DAY_TIME_SPECS = [dtm.time(12, 42, tzinfo=dtm.timezone(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
@@ -142,8 +142,16 @@ class TestHelpers(object):
# 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_from_timestamp_naive(self):
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None)
assert helpers.from_timestamp(1573431976, tzinfo=None) == datetime
def test_from_timestamp_aware(self, timezone):
# 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.from_timestamp(1573431976.1 - timezone.utcoffset(None).total_seconds())
== datetime)
def test_create_deep_linked_url(self):
username = 'JamesTheMock'
+9 -4
View File
@@ -31,6 +31,7 @@ from .test_document import document, document_file # noqa: F401
from .test_photo import _photo, photo_file, photo, thumb # noqa: F401
# noinspection PyUnresolvedReferences
from .test_video import video, video_file # noqa: F401
from tests.conftest import expect_bad_request
@pytest.fixture(scope='class')
@@ -320,10 +321,14 @@ class TestSendMediaGroup(object):
@pytest.mark.timeout(10) # noqa: F811
def test_send_media_group_new_files(self, bot, chat_id, video_file, photo_file, # noqa: F811
animation_file): # noqa: F811
messages = bot.send_media_group(chat_id, [
InputMediaVideo(video_file),
InputMediaPhoto(photo_file)
])
def func():
return bot.send_media_group(chat_id, [
InputMediaVideo(video_file),
InputMediaPhoto(photo_file)
])
messages = expect_bad_request(func, 'Type of file mismatch',
'Telegram did not accept the file.')
assert isinstance(messages, list)
assert len(messages) == 2
assert all([isinstance(mes, Message) for mes in messages])
+169 -7
View File
@@ -16,6 +16,7 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import calendar
import datetime as dtm
import os
import sys
@@ -25,10 +26,8 @@ from time import sleep
import pytest
from flaky import flaky
from telegram.ext import JobQueue, Updater, Job, CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import _UtcOffsetTimezone, _UTC
@pytest.fixture(scope='function')
@@ -276,7 +275,7 @@ class TestJobQueue(object):
# 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_tzinfo = dtm.timezone(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()
@@ -288,6 +287,69 @@ class TestJobQueue(object):
assert self.result == 1
assert job_queue._queue.get(False)[0] == pytest.approx(expected_reschedule_time)
def test_run_monthly(self, job_queue):
delta, now = 0.1, time.time()
date_time = dtm.datetime.utcfromtimestamp(now)
time_of_day = (date_time + dtm.timedelta(seconds=delta)).time()
expected_reschedule_time = now + delta
day = date_time.day
expected_reschedule_time += calendar.monthrange(date_time.year,
date_time.month)[1] * 24 * 60 * 60
job_queue.run_monthly(self.job_run_once, time_of_day, day)
sleep(0.2)
assert self.result == 1
assert job_queue._queue.get(False)[0] == pytest.approx(expected_reschedule_time)
def test_run_monthly_and_not_strict(self, job_queue):
# This only really tests something in months with < 31 days.
# But the trouble of patching datetime is probably not worth it
delta, now = 0.1, time.time()
date_time = dtm.datetime.utcfromtimestamp(now)
time_of_day = (date_time + dtm.timedelta(seconds=delta)).time()
expected_reschedule_time = now + delta
day = date_time.day
date_time += dtm.timedelta(calendar.monthrange(date_time.year,
date_time.month)[1] - day)
# next job should be scheduled on last day of month if day_is_strict is False
expected_reschedule_time += (calendar.monthrange(date_time.year,
date_time.month)[1] - day) * 24 * 60 * 60
job_queue.run_monthly(self.job_run_once, time_of_day, 31, day_is_strict=False)
assert job_queue._queue.get(False)[0] == pytest.approx(expected_reschedule_time)
def test_run_monthly_with_timezone(self, job_queue):
"""test that the day 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 = dtm.timezone(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_day = target_datetime.day
expected_reschedule_time = now + delta
expected_reschedule_time += calendar.monthrange(target_datetime.year,
target_datetime.month)[1] * 24 * 60 * 60
job_queue.run_monthly(self.job_run_once, target_time, target_day)
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)
with pytest.raises(ValueError, match='can not be set to'):
@@ -298,18 +360,21 @@ class TestJobQueue(object):
with pytest.raises(ValueError, match='can not be'):
j.interval = None
j.repeat = False
with pytest.raises(ValueError, match='must be of type'):
with pytest.raises(TypeError, match='must be of type'):
j.interval = 'every 3 minutes'
j.interval = 15
assert j.interval_seconds == 15
with pytest.raises(ValueError, match='argument should be of type'):
with pytest.raises(TypeError, match='argument should be of type'):
j.days = 'every day'
with pytest.raises(ValueError, match='The elements of the'):
with pytest.raises(TypeError, match='The elements of the'):
j.days = ('mon', 'wed')
with pytest.raises(ValueError, match='from 0 up to and'):
j.days = (0, 6, 12, 14)
with pytest.raises(TypeError, match='argument should be one of the'):
j._set_next_t('tomorrow')
def test_get_jobs(self, job_queue):
job1 = job_queue.run_once(self.job_run_once, 10, name='name1')
job2 = job_queue.run_once(self.job_run_once, 10, name='name1')
@@ -340,4 +405,101 @@ class TestJobQueue(object):
jobs = [job_1, job_2, job_3]
for job in jobs:
assert job.tzinfo == _UTC
assert job.tzinfo == dtm.timezone.utc
def test_job_next_t_property(self, job_queue):
# Testing:
# - next_t values match values from self._queue.queue (for run_once and run_repeating jobs)
# - next_t equals None if job is removed or if it's already ran
job1 = job_queue.run_once(self.job_run_once, 0.06, name='run_once job')
job2 = job_queue.run_once(self.job_run_once, 0.06, name='canceled run_once job')
job_queue.run_repeating(self.job_run_once, 0.04, name='repeatable job')
sleep(0.05)
job2.schedule_removal()
with job_queue._queue.mutex:
for t, job in job_queue._queue.queue:
t = dtm.datetime.fromtimestamp(t, job.tzinfo)
if job.removed:
assert job.next_t is None
else:
assert job.next_t == t
assert self.result == 1
sleep(0.02)
assert self.result == 2
assert job1.next_t is None
assert job2.next_t is None
def test_job_set_next_t(self, job_queue):
# Testing next_t setter for 'datetime.datetime' values
job = job_queue.run_once(self.job_run_once, 0.05)
t = dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=12)))
job._set_next_t(t)
job.tzinfo = dtm.timezone(dtm.timedelta(hours=5))
assert job.next_t == t.astimezone(job.tzinfo)
def test_passing_tzinfo_to_job(self, job_queue):
"""Test that tzinfo is correctly passed to job with run_once, run_daily, run_repeating
and run_monthly methods"""
when_dt_tz_specific = dtm.datetime.now(
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)
when_dt_tz_utc = dtm.datetime.now() + dtm.timedelta(seconds=2)
job_once1 = job_queue.run_once(self.job_run_once, when_dt_tz_specific)
job_once2 = job_queue.run_once(self.job_run_once, when_dt_tz_utc)
when_time_tz_specific = (dtm.datetime.now(
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)).timetz()
when_time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
job_once3 = job_queue.run_once(self.job_run_once, when_time_tz_specific)
job_once4 = job_queue.run_once(self.job_run_once, when_time_tz_utc)
first_dt_tz_specific = dtm.datetime.now(
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)
first_dt_tz_utc = dtm.datetime.now() + dtm.timedelta(seconds=2)
job_repeating1 = job_queue.run_repeating(
self.job_run_once, 2, first=first_dt_tz_specific)
job_repeating2 = job_queue.run_repeating(
self.job_run_once, 2, first=first_dt_tz_utc)
first_time_tz_specific = (dtm.datetime.now(
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)).timetz()
first_time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
job_repeating3 = job_queue.run_repeating(
self.job_run_once, 2, first=first_time_tz_specific)
job_repeating4 = job_queue.run_repeating(
self.job_run_once, 2, first=first_time_tz_utc)
time_tz_specific = (dtm.datetime.now(
tz=dtm.timezone(dtm.timedelta(hours=12))
) + dtm.timedelta(seconds=2)).timetz()
time_tz_utc = (dtm.datetime.now() + dtm.timedelta(seconds=2)).timetz()
job_daily1 = job_queue.run_daily(self.job_run_once, time_tz_specific)
job_daily2 = job_queue.run_daily(self.job_run_once, time_tz_utc)
job_monthly1 = job_queue.run_monthly(self.job_run_once, time_tz_specific, 1)
job_monthly2 = job_queue.run_monthly(self.job_run_once, time_tz_utc, 1)
assert job_once1.tzinfo == when_dt_tz_specific.tzinfo
assert job_once2.tzinfo == dtm.timezone.utc
assert job_once3.tzinfo == when_time_tz_specific.tzinfo
assert job_once4.tzinfo == dtm.timezone.utc
assert job_repeating1.tzinfo == first_dt_tz_specific.tzinfo
assert job_repeating2.tzinfo == dtm.timezone.utc
assert job_repeating3.tzinfo == first_time_tz_specific.tzinfo
assert job_repeating4.tzinfo == dtm.timezone.utc
assert job_daily1.tzinfo == time_tz_specific.tzinfo
assert job_daily2.tzinfo == dtm.timezone.utc
assert job_monthly1.tzinfo == time_tz_specific.tzinfo
assert job_monthly2.tzinfo == dtm.timezone.utc
+2 -2
View File
@@ -92,13 +92,13 @@ def message(bot):
options=[PollOption(text='a', voter_count=1),
PollOption(text='b', voter_count=2)], is_closed=False,
total_voter_count=0, is_anonymous=False, type=Poll.REGULAR,
allows_multiple_answers=True)},
allows_multiple_answers=True, explanation_entities=[])},
{'text': 'a text message', 'reply_markup': {'inline_keyboard': [[{
'text': 'start', 'url': 'http://google.com'}, {
'text': 'next', 'callback_data': 'abcd'}],
[{'text': 'Cancel', 'callback_data': 'Cancel'}]]}},
{'quote': True},
{'dice': Dice(4)}
{'dice': Dice(4, '🎲')}
],
ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text',
'caption_entities', 'audio', 'document', 'animation', 'game', 'photo',
+34 -19
View File
@@ -51,7 +51,33 @@ def change_directory(tmp_path):
@pytest.fixture(scope="function")
def base_persistence():
return BasePersistence(store_chat_data=True, store_user_data=True, store_bot_data=True)
class OwnPersistence(BasePersistence):
def get_bot_data(self):
raise NotImplementedError
def get_chat_data(self):
raise NotImplementedError
def get_user_data(self):
raise NotImplementedError
def get_conversations(self, name):
raise NotImplementedError
def update_bot_data(self, data):
raise NotImplementedError
def update_chat_data(self, chat_id, data):
raise NotImplementedError
def update_conversation(self, name, key, new_state):
raise NotImplementedError
def update_user_data(self, user_id, data):
raise NotImplementedError
return OwnPersistence(store_chat_data=True, store_user_data=True, store_bot_data=True)
@pytest.fixture(scope="function")
@@ -100,22 +126,13 @@ class TestBasePersistence(object):
def test_creation(self, base_persistence):
assert base_persistence.store_chat_data
assert base_persistence.store_user_data
with pytest.raises(NotImplementedError):
base_persistence.get_bot_data()
with pytest.raises(NotImplementedError):
base_persistence.get_chat_data()
with pytest.raises(NotImplementedError):
base_persistence.get_user_data()
with pytest.raises(NotImplementedError):
base_persistence.get_conversations("test")
with pytest.raises(NotImplementedError):
base_persistence.update_bot_data(None)
with pytest.raises(NotImplementedError):
base_persistence.update_chat_data(None, None)
with pytest.raises(NotImplementedError):
base_persistence.update_user_data(None, None)
with pytest.raises(NotImplementedError):
base_persistence.update_conversation(None, None, None)
assert base_persistence.store_bot_data
def test_abstract_methods(self):
with pytest.raises(TypeError, match=('get_bot_data, get_chat_data, get_conversations, '
'get_user_data, update_bot_data, update_chat_data, '
'update_conversation, update_user_data')):
BasePersistence()
def test_implementation(self, updater, base_persistence):
dp = updater.dispatcher
@@ -127,8 +144,6 @@ class TestBasePersistence(object):
with pytest.raises(ValueError, match="if dispatcher has no persistence"):
dp.add_handler(ConversationHandler([], {}, [], persistent=True, name="My Handler"))
dp.persistence = base_persistence
with pytest.raises(NotImplementedError):
dp.add_handler(ConversationHandler([], {}, [], persistent=True, name="My Handler"))
def test_dispatcher_integration_init(self, bot, base_persistence, chat_data, user_data,
bot_data):
+6 -2
View File
@@ -24,6 +24,7 @@ from flaky import flaky
from telegram import Sticker, TelegramError, PhotoSize, InputFile
from telegram.utils.helpers import escape_markdown
from tests.conftest import expect_bad_request
@pytest.fixture(scope='function')
@@ -35,8 +36,11 @@ def photo_file():
@pytest.fixture(scope='class')
def _photo(bot, chat_id):
with open('tests/data/telegram.jpg', 'rb') as f:
return bot.send_photo(chat_id, photo=f, timeout=50).photo
def func():
with open('tests/data/telegram.jpg', 'rb') as f:
return bot.send_photo(chat_id, photo=f, timeout=50).photo
return expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.')
@pytest.fixture(scope='class')
+41 -3
View File
@@ -19,7 +19,9 @@
import pytest
from telegram import Poll, PollOption, PollAnswer, User
from datetime import datetime
from telegram import Poll, PollOption, PollAnswer, User, MessageEntity
from telegram.utils.helpers import to_timestamp
@pytest.fixture(scope="class")
@@ -91,7 +93,11 @@ def poll():
TestPoll.is_closed,
TestPoll.is_anonymous,
TestPoll.type,
TestPoll.allows_multiple_answers
TestPoll.allows_multiple_answers,
explanation=TestPoll.explanation,
explanation_entities=TestPoll.explanation_entities,
open_period=TestPoll.open_period,
close_date=TestPoll.close_date,
)
@@ -104,6 +110,11 @@ class TestPoll(object):
is_anonymous = False
type = Poll.REGULAR
allows_multiple_answers = True
explanation = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467'
b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape')
explanation_entities = [MessageEntity(13, 17, MessageEntity.URL)]
open_period = 42
close_date = datetime.utcnow()
def test_de_json(self):
json_dict = {
@@ -114,7 +125,11 @@ class TestPoll(object):
'is_closed': self.is_closed,
'is_anonymous': self.is_anonymous,
'type': self.type,
'allows_multiple_answers': self.allows_multiple_answers
'allows_multiple_answers': self.allows_multiple_answers,
'explanation': self.explanation,
'explanation_entities': [self.explanation_entities[0].to_dict()],
'open_period': self.open_period,
'close_date': to_timestamp(self.close_date)
}
poll = Poll.de_json(json_dict, None)
@@ -130,6 +145,11 @@ class TestPoll(object):
assert poll.is_anonymous == self.is_anonymous
assert poll.type == self.type
assert poll.allows_multiple_answers == self.allows_multiple_answers
assert poll.explanation == self.explanation
assert poll.explanation_entities == self.explanation_entities
assert poll.open_period == self.open_period
assert pytest.approx(poll.close_date == self.close_date)
assert to_timestamp(poll.close_date) == to_timestamp(self.close_date)
def test_to_dict(self, poll):
poll_dict = poll.to_dict()
@@ -143,3 +163,21 @@ class TestPoll(object):
assert poll_dict['is_anonymous'] == poll.is_anonymous
assert poll_dict['type'] == poll.type
assert poll_dict['allows_multiple_answers'] == poll.allows_multiple_answers
assert poll_dict['explanation'] == poll.explanation
assert poll_dict['explanation_entities'] == [poll.explanation_entities[0].to_dict()]
assert poll_dict['open_period'] == poll.open_period
assert poll_dict['close_date'] == to_timestamp(poll.close_date)
def test_parse_entity(self, poll):
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
poll.explanation_entities = [entity]
assert poll.parse_explanation_entity(entity) == 'http://google.com'
def test_parse_entities(self, poll):
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1)
poll.explanation_entities = [entity_2, entity]
assert poll.parse_explanation_entities(MessageEntity.URL) == {entity: 'http://google.com'}
assert poll.parse_explanation_entities() == {entity: 'http://google.com', entity_2: 'h'}
+11 -2
View File
@@ -25,6 +25,7 @@ from flaky import flaky
from future.utils import PY2
from telegram import Sticker, PhotoSize, TelegramError, StickerSet, Audio, MaskPosition
from telegram.error import BadRequest
@pytest.fixture(scope='function')
@@ -260,7 +261,11 @@ class TestSticker(object):
def sticker_set(bot):
ss = bot.get_sticker_set('test_by_{}'.format(bot.username))
if len(ss.stickers) > 100:
raise Exception('stickerset is growing too large.')
try:
for i in range(1, 50):
bot.delete_sticker_from_set(ss.stickers[-i].file_id)
except BadRequest:
raise Exception('stickerset is growing too large.')
return ss
@@ -268,7 +273,11 @@ def sticker_set(bot):
def animated_sticker_set(bot):
ss = bot.get_sticker_set('animated_test_by_{}'.format(bot.username))
if len(ss.stickers) > 100:
raise Exception('stickerset is growing too large.')
try:
for i in range(1, 50):
bot.delete_sticker_from_set(ss.stickers[-i].file_id)
except BadRequest:
raise Exception('stickerset is growing too large.')
return ss
+2 -2
View File
@@ -41,7 +41,7 @@ from future.builtins import bytes
from telegram import TelegramError, Message, User, Chat, Update, Bot
from telegram.error import Unauthorized, InvalidToken, TimedOut, RetryAfter
from telegram.ext import Updater, Dispatcher, BasePersistence
from telegram.ext import Updater, Dispatcher, DictPersistence
signalskip = pytest.mark.skipif(sys.platform == 'win32',
reason='Can\'t send signals without stopping '
@@ -467,7 +467,7 @@ class TestUpdater(object):
def test_mutual_exclude_persistence_dispatcher(self):
dispatcher = Dispatcher(None, None)
persistence = BasePersistence()
persistence = DictPersistence()
with pytest.raises(ValueError):
Updater(dispatcher=dispatcher, persistence=persistence)