Merge remote-tracking branch 'origin/master' into default_parse_mode

This commit is contained in:
Hinrich mahler 2019-11-15 15:07:59 +00:00
commit b83376a3fe
25 changed files with 455 additions and 75 deletions

View file

@ -25,7 +25,7 @@ Setting things up
.. code-block:: bash .. 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: 5. Install pre-commit hooks:

View file

@ -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! 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. Use this template to notify us if you found a bug.
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
To make it easier for us to help you please enter detailed information below. To make it easier for us to help you please enter detailed information below.

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
.github/ISSUE_TEMPLATE/question.md vendored Normal file
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
.github/workflows/example_notifier.yml vendored Normal file
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 }}

92
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,92 @@
name: Testing your PR
on:
pull_request:
branches:
- master
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
test-pre-commit: 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}

View file

@ -26,6 +26,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `d-qoi <https://github.com/d-qoi>`_ - `d-qoi <https://github.com/d-qoi>`_
- `daimajia <https://github.com/daimajia>`_ - `daimajia <https://github.com/daimajia>`_
- `Daniel Reed <https://github.com/nmlorg>`_ - `Daniel Reed <https://github.com/nmlorg>`_
- `Dmitry Grigoryev <https://github.com/icecom-dg>`_
- `Ehsan Online <https://github.com/ehsanonline>`_ - `Ehsan Online <https://github.com/ehsanonline>`_
- `Eli Gao <https://github.com/eligao>`_ - `Eli Gao <https://github.com/eligao>`_
- `Emilio Molinari <https://github.com/xates>`_ - `Emilio Molinari <https://github.com/xates>`_

View file

@ -192,11 +192,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! 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>`_.
============ ============

View file

@ -1,6 +1,6 @@
# Examples # 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. 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.

View file

@ -21,7 +21,7 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def msg(bot, update): def msg(update, context):
# If we received any passport data # If we received any passport data
passport_data = update.message.passport_data passport_data = update.message.passport_data
if passport_data: if passport_data:
@ -77,9 +77,9 @@ def msg(bot, update):
actual_file.download() actual_file.download()
def error(bot, update, error): def error(update, context):
"""Log Errors caused by Updates.""" """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(): def main():

View file

@ -42,7 +42,7 @@ def start_with_shipping_callback(update, context):
currency = "USD" currency = "USD"
# price in dollars # price in dollars
price = 1 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 # check https://core.telegram.org/bots/payments#supported-currencies for more details
prices = [LabeledPrice("Test", price * 100)] prices = [LabeledPrice("Test", price * 100)]
@ -66,7 +66,7 @@ def start_without_shipping_callback(update, context):
currency = "USD" currency = "USD"
# price in dollars # price in dollars
price = 1 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)] prices = [LabeledPrice("Test", price * 100)]
# optionally pass need_name=True, need_phone_number=True, # optionally pass need_name=True, need_phone_number=True,

View file

@ -17,7 +17,8 @@
"""Constants in the Telegram network. """Constants in the Telegram network.
The following constants were extracted from the 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: Attributes:
MAX_MESSAGE_LENGTH (:obj:`int`): 4096 MAX_MESSAGE_LENGTH (:obj:`int`): 4096
@ -25,6 +26,7 @@ Attributes:
SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443] SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD (:obj:`int`): In bytes (20MB) MAX_FILESIZE_DOWNLOAD (:obj:`int`): In bytes (20MB)
MAX_FILESIZE_UPLOAD (:obj:`int`): In bytes (50MB) 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 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. over this limit, but eventually you'll begin receiving 429 errors.
MAX_MESSAGES_PER_SECOND (:obj:`int`): 30 MAX_MESSAGES_PER_SECOND (:obj:`int`): 30
@ -47,6 +49,7 @@ MAX_CAPTION_LENGTH = 1024
SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443] SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB) MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB)
MAX_FILESIZE_UPLOAD = int(50E6) # (50MB) MAX_FILESIZE_UPLOAD = int(50E6) # (50MB)
MAX_PHOTOSIZE_UPLOAD = int(10E6) # (10MB)
MAX_MESSAGES_PER_SECOND_PER_CHAT = 1 MAX_MESSAGES_PER_SECOND_PER_CHAT = 1
MAX_MESSAGES_PER_SECOND = 30 MAX_MESSAGES_PER_SECOND = 30
MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20 MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20

View file

@ -20,6 +20,7 @@
import logging import logging
import warnings import warnings
from threading import Lock
from telegram import Update from telegram import Update
from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler, from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
@ -37,8 +38,7 @@ class _ConversationTimeoutContext(object):
class ConversationHandler(Handler): class ConversationHandler(Handler):
""" """
A handler to hold a conversation with a single user by managing four collections of other 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 handlers.
users are managed by instances of this class.
The first collection, a ``list`` named :attr:`entry_points`, is used to initiate the The first collection, a ``list`` named :attr:`entry_points`, is used to initiate the
conversation, for example with a :class:`telegram.ext.CommandHandler` or 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.map_to_parent = map_to_parent
self.timeout_jobs = dict() self.timeout_jobs = dict()
self._timeout_jobs_lock = Lock()
self.conversations = dict() self.conversations = dict()
self._conversations_lock = Lock()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
@ -262,6 +264,7 @@ class ConversationHandler(Handler):
return None return None
key = self._get_key(update) key = self._get_key(update)
with self._conversations_lock:
state = self.conversations.get(key) state = self.conversations.get(key)
# Resolve promises # Resolve promises
@ -281,6 +284,7 @@ class ConversationHandler(Handler):
if res is None and old_state is None: if res is None and old_state is None:
res = self.END res = self.END
self.update_state(res, key) self.update_state(res, key)
with self._conversations_lock:
state = self.conversations.get(key) state = self.conversations.get(key)
else: else:
handlers = self.states.get(self.WAITING, []) handlers = self.states.get(self.WAITING, [])
@ -340,12 +344,19 @@ class ConversationHandler(Handler):
""" """
conversation_key, handler, check_result = check_result conversation_key, handler, check_result = check_result
new_state = handler.handle_update(update, dispatcher, check_result, context)
with self._timeout_jobs_lock:
# Remove the old timeout job (if present)
timeout_job = self.timeout_jobs.pop(conversation_key, None) timeout_job = self.timeout_jobs.pop(conversation_key, None)
if timeout_job is not None: if timeout_job is not None:
timeout_job.schedule_removal() 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: 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.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
self._trigger_timeout, self.conversation_timeout, self._trigger_timeout, self.conversation_timeout,
context=_ConversationTimeoutContext(conversation_key, update, dispatcher)) context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
@ -358,6 +369,7 @@ class ConversationHandler(Handler):
def update_state(self, new_state, key): def update_state(self, new_state, key):
if new_state == self.END: if new_state == self.END:
with self._conversations_lock:
if key in self.conversations: if key in self.conversations:
# If there is no key in conversations, nothing is done. # If there is no key in conversations, nothing is done.
del self.conversations[key] del self.conversations[key]
@ -365,12 +377,14 @@ class ConversationHandler(Handler):
self.persistence.update_conversation(self.name, key, None) self.persistence.update_conversation(self.name, key, None)
elif isinstance(new_state, Promise): elif isinstance(new_state, Promise):
with self._conversations_lock:
self.conversations[key] = (self.conversations.get(key), new_state) self.conversations[key] = (self.conversations.get(key), new_state)
if self.persistent: if self.persistent:
self.persistence.update_conversation(self.name, key, self.persistence.update_conversation(self.name, key,
(self.conversations.get(key), new_state)) (self.conversations.get(key), new_state))
elif new_state is not None: elif new_state is not None:
with self._conversations_lock:
self.conversations[key] = new_state self.conversations[key] = new_state
if self.persistent: if self.persistent:
self.persistence.update_conversation(self.name, key, new_state) self.persistence.update_conversation(self.name, key, new_state)
@ -380,11 +394,17 @@ class ConversationHandler(Handler):
# Backward compatibility with bots that do not use CallbackContext # Backward compatibility with bots that do not use CallbackContext
if isinstance(context, CallbackContext): if isinstance(context, CallbackContext):
context = context.job.context job = context.job
else:
context = job.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, []) handlers = self.states.get(self.TIMEOUT, [])
for handler in handlers: for handler in handlers:
check = handler.check_update(context.update) check = handler.check_update(context.update)

View file

@ -909,6 +909,39 @@ officedocument.wordprocessingml.document")``-
return message.from_user.language_code and any( return message.from_user.language_code and any(
[message.from_user.language_code.startswith(x) for x in self.lang]) [message.from_user.language_code.startswith(x) for x in self.lang])
class msg_in(BaseFilter):
"""Filters messages to only allow those whose text/caption appears in a given list.
Examples:
A simple usecase 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.msg_in(buttons), callback_method)
Args:
list_ (List[:obj:`str`]): Which messages to allow through. Only exact matches
are allowed.
caption (:obj:`bool`): Optional. Whether the caption should be used instead of text.
Default is ``False``.
"""
def __init__(self, list_, caption=False):
self.list_ = list_
self.caption = caption
self.name = 'Filters.msg_in({!r}, caption={!r})'.format(self.list_, self.caption)
def filter(self, message):
if self.caption:
txt = message.caption
else:
txt = message.text
return txt in self.list_
class _UpdateType(BaseFilter): class _UpdateType(BaseFilter):
update_filter = True update_filter = True

View file

@ -42,7 +42,8 @@ class JobQueue(object):
Attributes: Attributes:
_queue (:obj:`PriorityQueue`): The queue that holds the Jobs. _queue (:obj:`PriorityQueue`): The queue that holds the Jobs.
bot (:class:`telegram.Bot`): The bot instance that should be passed to 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): def __init__(self, bot=None):
@ -68,6 +69,13 @@ class JobQueue(object):
self._running = False self._running = False
def set_dispatcher(self, dispatcher): 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 self._dispatcher = dispatcher
def _put(self, job, next_t=None, last_t=None): def _put(self, job, next_t=None, last_t=None):

View file

@ -742,13 +742,14 @@ class Message(TelegramObject):
self._quote(kwargs) self._quote(kwargs)
return self.bot.send_poll(self.chat_id, *args, **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:: """Shortcut for::
bot.forward_message(chat_id=chat_id, bot.forward_message(chat_id=chat_id,
from_chat_id=update.message.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: Returns:
:class:`telegram.Message`: On success, instance representing the message forwarded. :class:`telegram.Message`: On success, instance representing the message forwarded.
@ -757,8 +758,9 @@ class Message(TelegramObject):
return self.bot.forward_message( return self.bot.forward_message(
chat_id=chat_id, chat_id=chat_id,
from_chat_id=self.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): def edit_text(self, *args, **kwargs):
"""Shortcut for:: """Shortcut for::

View file

@ -17,11 +17,10 @@
# You should have received a copy of the GNU Lesser Public License # You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]. # along with this program. If not, see [http://www.gnu.org/licenses/].
"""Provide a bot to tests""" """Provide a bot to tests"""
import json
import base64
import os import os
import random 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 # 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 # 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', 'payment_provider_token': '284685063:TEST:NjQ0NjZlNzI5YjJi',
'chat_id': '675666224', 'chat_id': '675666224',
'super_group_id': '-1001493296829', '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', 'token': '558194066:AAEEylntuKSLXj9odiv3TnX7Z5KY2J3zY3M',
'payment_provider_token': '284685063:TEST:YjEwODQwMTFmNDcy', 'payment_provider_token': '284685063:TEST:YjEwODQwMTFmNDcy',
'chat_id': '675666224', 'chat_id': '675666224',
'super_group_id': '-1001493296829', '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): def get(name, fallback):
full_name = '{0}_{1}_{2[0]}{2[1]}'.format(name, python_implementation(), # If we have TOKEN, PAYMENT_PROVIDER_TOKEN, CHAT_ID, SUPER_GROUP_ID,
sys.version_info).upper() # CHANNEL_ID, BOT_NAME, or BOT_USERNAME in the environment, then use that
# 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
val = os.getenv(name.upper()) val = os.getenv(name.upper())
if val: if val:
return 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 # Otherwise go with the fallback
return fallback return fallback

View file

@ -38,6 +38,11 @@ TRAVIS = os.getenv('TRAVIS', False)
if TRAVIS: if TRAVIS:
pytest_plugins = ['tests.travis_fold'] 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 # THIS KEY IS OBVIOUSLY COMPROMISED
# DO NOT USE IN PRODUCTION! # 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 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

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]')

View file

@ -114,13 +114,14 @@ class TestBot(object):
@pytest.mark.timeout(10) @pytest.mark.timeout(10)
def test_delete_message(self, bot, chat_id): def test_delete_message(self, bot, chat_id):
message = bot.send_message(chat_id, text='will be deleted') 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 assert bot.delete_message(chat_id=chat_id, message_id=message.message_id) is True
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.timeout(10) @pytest.mark.timeout(10)
def test_delete_message_old_message(self, bot, chat_id): 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 # Considering that the first message is old enough
bot.delete_message(chat_id=chat_id, message_id=1) bot.delete_message(chat_id=chat_id, message_id=1)

View file

@ -613,6 +613,53 @@ class TestConversationHandler(object):
assert handler.conversations.get((self.group.id, user1.id)) is None assert handler.conversations.get((self.group.id, user1.id)) is None
assert not self.is_timeout 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): def test_per_message_warning_is_only_shown_once(self, recwarn):
ConversationHandler( ConversationHandler(
entry_points=self.entry_points, entry_points=self.entry_points,

View file

@ -604,6 +604,16 @@ class TestFilters(object):
update.message.from_user.language_code = 'da' update.message.from_user.language_code = 'da'
assert f(update) assert f(update)
def test_msg_in_filter(self, update):
update.message.text = 'test'
update.message.caption = 'caption'
assert Filters.msg_in(['test'])(update)
assert Filters.msg_in(['caption'], caption=True)(update)
assert not Filters.msg_in(['test'], caption=True)(update)
assert not Filters.msg_in(['caption'])(update)
def test_and_filters(self, update): def test_and_filters(self, update):
update.message.text = 'test' update.message.text = 'test'
update.message.forward_date = datetime.datetime.now() update.message.forward_date = datetime.datetime.now()

View file

@ -40,6 +40,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('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 @flaky(10, 1) # Timings aren't quite perfect
class TestJobQueue(object): class TestJobQueue(object):
result = 0 result = 0

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('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): class TestDelayQueue(object):
N = 128 N = 128
burst_limit = 30 burst_limit = 30

View file

@ -17,8 +17,6 @@
# You should have received a copy of the GNU Lesser Public License # You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]. # along with this program. If not, see [http://www.gnu.org/licenses/].
import os import os
import sys
from platform import python_implementation
import pytest import pytest
@ -30,17 +28,12 @@ def call_pre_commit_hook(hook_id):
@pytest.mark.nocoverage @pytest.mark.nocoverage
@pytest.mark.parametrize('hook_id', argvalues=('yapf', 'flake8', 'pylint')) @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 os.getenv('TEST_PRE_COMMIT', False), reason='TEST_PRE_COMMIT not enabled')
@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.')
def test_pre_commit_hook(hook_id): def test_pre_commit_hook(hook_id):
assert call_pre_commit_hook(hook_id) == 0 # pragma: no cover assert call_pre_commit_hook(hook_id) == 0 # pragma: no cover
@pytest.mark.nocoverage @pytest.mark.nocoverage
@pytest.mark.skipif( @pytest.mark.skipif(not os.getenv('TEST_BUILD', False), reason='TEST_BUILD not enabled')
not sys.version_info[:2] in ((3, 6), (2, 7)) or python_implementation() != 'CPython',
reason='Only testing build on 2.7 and 3.6')
def test_build(): def test_build():
assert os.system('python setup.py bdist_dumb') == 0 # pragma: no cover assert os.system('python setup.py bdist_dumb') == 0 # pragma: no cover