├── .gitignore ├── AUTHORS ├── ChangeLog ├── LICENSE ├── README.rst ├── requirements.txt ├── retry ├── __init__.py ├── api.py └── compat.py ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests └── test_retry.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info/ 3 | *.log 4 | *.manifest 5 | *.mo 6 | *.pot 7 | *.py[cod] 8 | *.so 9 | *.spec 10 | .cache 11 | .coverage 12 | .coverage.* 13 | .eggs/ 14 | .idea/ 15 | .installed.cfg 16 | .Python 17 | .tox/ 18 | build/ 19 | coverage.xml 20 | develop-eggs/ 21 | dist/ 22 | docs/_build/ 23 | downloads/ 24 | eggs/ 25 | env/ 26 | htmlcov/ 27 | lib/ 28 | lib64/ 29 | nosetests.xml 30 | parts/ 31 | pip-delete-this-directory.txt 32 | pip-log.txt 33 | sdist/ 34 | target/ 35 | var/ 36 | __pycache__/ 37 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Rémy 2 | Rémy Greinhofer 3 | invlpg 4 | Richard O'Dwyer 5 | williara 6 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | 0.9.2 5 | ----- 6 | 7 | * Updating requirements.txt to allow for any decorators >=3.4.2 8 | 9 | 0.9.1 10 | ----- 11 | 12 | * Fix dependency issues with other packages caused by explicit dep verions in requirements 13 | 14 | 0.9.0 15 | ----- 16 | 17 | * Add AUTHORS and ChangeLog files 18 | * Packaging the application using PBR 19 | * Update documentation 20 | * Add retry_call function 21 | * Update tox.ini 22 | * Update requirements 23 | * Move the tests to the appropriate package 24 | * Add .gitignore file 25 | 26 | 0.8.1 27 | ----- 28 | 29 | * v0.8.1 30 | * Move try/import to compat 31 | 32 | 0.8.0 33 | ----- 34 | 35 | * v0.8.0 36 | * dos2unix 37 | * Add argument jitter for retry() 38 | * Add argument max_delay for retry() 39 | * Refactor retry() 40 | 41 | 0.7.0 42 | ----- 43 | 44 | * v0.7.0 45 | * Update README.rst 46 | * retry(): Update docstring 47 | * retry(): Change default tries to -1 48 | * Move decorator() to .compat 49 | * Add test_tries_minus1() 50 | * Add test_tries_inf() 51 | * Mock time.sleep in test case 52 | * Refactor retry() 53 | 54 | 0.6.0 55 | ----- 56 | 57 | * v0.6.0 58 | * Fix inaccurate attempt counter 59 | * logger is now optional 60 | * Extract logging_logger 61 | * Make decorator module optional 62 | 63 | 0.5.0 64 | ----- 65 | 66 | * v0.5.0 67 | * Update README.rst 68 | * Add badges 69 | * Configurable logger 70 | * Update classifiers 71 | * Remove .hgignore and .hgtags 72 | * Faster test 73 | * Support Python 3.4 74 | * Add tox.ini 75 | * Extract retry/api.py 76 | * Require pytest 77 | * Add test_retry.py 78 | * Added tag 0.4.2 for changeset 315f5f1229f6 79 | 80 | 0.4.2 81 | ----- 82 | 83 | * Version 0.4.2 84 | * python2.6 support 85 | * README.rst: Add installation 86 | * Add classifiers 87 | * Add LICENSE 88 | * Add requirements.txt 89 | * Fix rST h1 h2 for README.rst 90 | * Add url 91 | * Add README.rst 92 | * Ignore *.egg-info 93 | * Ignore .env 94 | * Ignore .ropeproject 95 | * Ignore .git 96 | * Added tag 0.4.1 for changeset 2765d4be1b4d 97 | 98 | 0.4.1 99 | ----- 100 | 101 | * Version 0.4.1 102 | * Add license 103 | * Add long_description 104 | * Add docstring for retry() 105 | * Added tag 0.4.0 for changeset e053cae4b105 106 | 107 | 0.4.0 108 | ----- 109 | 110 | * Version 0.4.0 111 | * Convert to a package 112 | * backoff defaults to 1 113 | * Refactor: tries defaults to inf instead of None 114 | * Added tag 0.3.0 for changeset 60960fe42471 115 | 116 | 0.3.0 117 | ----- 118 | 119 | * Version 0.3.0 120 | * delay defaults to 0 121 | * Use logging 122 | * tries can be None, for retry forever 123 | * Add ifmain 124 | * Refactor 125 | * Added tag 0.2.0 for changeset 87b6dd2bc30b 126 | 127 | 0.2.0 128 | ----- 129 | 130 | * Fix typo 131 | * Refactor 132 | * Add description 133 | * Use decorator 134 | * Added tag 0.1.0 for changeset bf83184dd128 135 | 136 | 0.1.0 137 | ----- 138 | 139 | * init 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 invl 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | retry 2 | ===== 3 | 4 | .. image:: https://img.shields.io/pypi/dm/retry.svg?maxAge=2592000 5 | :target: https://pypi.python.org/pypi/retry/ 6 | 7 | .. image:: https://img.shields.io/pypi/v/retry.svg?maxAge=2592000 8 | :target: https://pypi.python.org/pypi/retry/ 9 | 10 | .. image:: https://img.shields.io/pypi/l/retry.svg?maxAge=2592000 11 | :target: https://pypi.python.org/pypi/retry/ 12 | 13 | 14 | Easy to use retry decorator. 15 | 16 | 17 | Features 18 | -------- 19 | 20 | - No external dependency (stdlib only). 21 | - (Optionally) Preserve function signatures (`pip install decorator`). 22 | - Original traceback, easy to debug. 23 | 24 | 25 | Installation 26 | ------------ 27 | 28 | .. code-block:: bash 29 | 30 | $ pip install retry 31 | 32 | 33 | API 34 | --- 35 | 36 | retry decorator 37 | ^^^^^^^^^^^^^^^ 38 | 39 | .. code:: python 40 | 41 | def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger): 42 | """Return a retry decorator. 43 | 44 | :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. 45 | :param tries: the maximum number of attempts. default: -1 (infinite). 46 | :param delay: initial delay between attempts. default: 0. 47 | :param max_delay: the maximum value of delay. default: None (no limit). 48 | :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff). 49 | :param jitter: extra seconds added to delay between attempts. default: 0. 50 | fixed if a number, random if a range tuple (min, max) 51 | :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. 52 | default: retry.logging_logger. if None, logging is disabled. 53 | """ 54 | 55 | Various retrying logic can be achieved by combination of arguments. 56 | 57 | 58 | Examples 59 | """""""" 60 | 61 | .. code:: python 62 | 63 | from retry import retry 64 | 65 | .. code:: python 66 | 67 | @retry() 68 | def make_trouble(): 69 | '''Retry until succeed''' 70 | 71 | .. code:: python 72 | 73 | @retry(ZeroDivisionError, tries=3, delay=2) 74 | def make_trouble(): 75 | '''Retry on ZeroDivisionError, raise error after 3 attempts, sleep 2 seconds between attempts.''' 76 | 77 | .. code:: python 78 | 79 | @retry((ValueError, TypeError), delay=1, backoff=2) 80 | def make_trouble(): 81 | '''Retry on ValueError or TypeError, sleep 1, 2, 4, 8, ... seconds between attempts.''' 82 | 83 | .. code:: python 84 | 85 | @retry((ValueError, TypeError), delay=1, backoff=2, max_delay=4) 86 | def make_trouble(): 87 | '''Retry on ValueError or TypeError, sleep 1, 2, 4, 4, ... seconds between attempts.''' 88 | 89 | .. code:: python 90 | 91 | @retry(ValueError, delay=1, jitter=1) 92 | def make_trouble(): 93 | '''Retry on ValueError, sleep 1, 2, 3, 4, ... seconds between attempts.''' 94 | 95 | .. code:: python 96 | 97 | # If you enable logging, you can get warnings like 'ValueError, retrying in 98 | # 1 seconds' 99 | if __name__ == '__main__': 100 | import logging 101 | logging.basicConfig() 102 | make_trouble() 103 | 104 | retry_call 105 | ^^^^^^^^^^ 106 | 107 | .. code:: python 108 | 109 | def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, 110 | jitter=0, 111 | logger=logging_logger): 112 | """ 113 | Calls a function and re-executes it if it failed. 114 | 115 | :param f: the function to execute. 116 | :param fargs: the positional arguments of the function to execute. 117 | :param fkwargs: the named arguments of the function to execute. 118 | :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. 119 | :param tries: the maximum number of attempts. default: -1 (infinite). 120 | :param delay: initial delay between attempts. default: 0. 121 | :param max_delay: the maximum value of delay. default: None (no limit). 122 | :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff). 123 | :param jitter: extra seconds added to delay between attempts. default: 0. 124 | fixed if a number, random if a range tuple (min, max) 125 | :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. 126 | default: retry.logging_logger. if None, logging is disabled. 127 | :returns: the result of the f function. 128 | """ 129 | 130 | This is very similar to the decorator, except that it takes a function and its arguments as parameters. The use case behind it is to be able to dynamically adjust the retry arguments. 131 | 132 | .. code:: python 133 | 134 | import requests 135 | 136 | from retry.api import retry_call 137 | 138 | 139 | def make_trouble(service, info=None): 140 | if not info: 141 | info = '' 142 | r = requests.get(service + info) 143 | return r.text 144 | 145 | 146 | def what_is_my_ip(approach=None): 147 | if approach == "optimistic": 148 | tries = 1 149 | elif approach == "conservative": 150 | tries = 3 151 | else: 152 | # skeptical 153 | tries = -1 154 | result = retry_call(make_trouble, fargs=["http://ipinfo.io/"], fkwargs={"info": "ip"}, tries=tries) 155 | print(result) 156 | 157 | what_is_my_ip("conservative") 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | decorator>=3.4.2 2 | py>=1.4.26,<2.0.0 3 | -------------------------------------------------------------------------------- /retry/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['retry', 'retry_call'] 2 | 3 | import logging 4 | 5 | from .api import retry, retry_call 6 | from .compat import NullHandler 7 | 8 | 9 | # Set default logging handler to avoid "No handler found" warnings. 10 | log = logging.getLogger(__name__) 11 | log.addHandler(NullHandler()) 12 | -------------------------------------------------------------------------------- /retry/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import time 4 | 5 | from functools import partial 6 | 7 | from .compat import decorator 8 | 9 | 10 | logging_logger = logging.getLogger(__name__) 11 | 12 | 13 | def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, 14 | logger=logging_logger): 15 | """ 16 | Executes a function and retries it if it failed. 17 | 18 | :param f: the function to execute. 19 | :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. 20 | :param tries: the maximum number of attempts. default: -1 (infinite). 21 | :param delay: initial delay between attempts. default: 0. 22 | :param max_delay: the maximum value of delay. default: None (no limit). 23 | :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff). 24 | :param jitter: extra seconds added to delay between attempts. default: 0. 25 | fixed if a number, random if a range tuple (min, max) 26 | :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. 27 | default: retry.logging_logger. if None, logging is disabled. 28 | :returns: the result of the f function. 29 | """ 30 | _tries, _delay = tries, delay 31 | while _tries: 32 | try: 33 | return f() 34 | except exceptions as e: 35 | _tries -= 1 36 | if not _tries: 37 | raise 38 | 39 | if logger is not None: 40 | logger.warning('%s, retrying in %s seconds...', e, _delay) 41 | 42 | time.sleep(_delay) 43 | _delay *= backoff 44 | 45 | if isinstance(jitter, tuple): 46 | _delay += random.uniform(*jitter) 47 | else: 48 | _delay += jitter 49 | 50 | if max_delay is not None: 51 | _delay = min(_delay, max_delay) 52 | 53 | 54 | def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger): 55 | """Returns a retry decorator. 56 | 57 | :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. 58 | :param tries: the maximum number of attempts. default: -1 (infinite). 59 | :param delay: initial delay between attempts. default: 0. 60 | :param max_delay: the maximum value of delay. default: None (no limit). 61 | :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff). 62 | :param jitter: extra seconds added to delay between attempts. default: 0. 63 | fixed if a number, random if a range tuple (min, max) 64 | :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. 65 | default: retry.logging_logger. if None, logging is disabled. 66 | :returns: a retry decorator. 67 | """ 68 | 69 | @decorator 70 | def retry_decorator(f, *fargs, **fkwargs): 71 | args = fargs if fargs else list() 72 | kwargs = fkwargs if fkwargs else dict() 73 | return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, 74 | logger) 75 | 76 | return retry_decorator 77 | 78 | 79 | def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, 80 | jitter=0, 81 | logger=logging_logger): 82 | """ 83 | Calls a function and re-executes it if it failed. 84 | 85 | :param f: the function to execute. 86 | :param fargs: the positional arguments of the function to execute. 87 | :param fkwargs: the named arguments of the function to execute. 88 | :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. 89 | :param tries: the maximum number of attempts. default: -1 (infinite). 90 | :param delay: initial delay between attempts. default: 0. 91 | :param max_delay: the maximum value of delay. default: None (no limit). 92 | :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff). 93 | :param jitter: extra seconds added to delay between attempts. default: 0. 94 | fixed if a number, random if a range tuple (min, max) 95 | :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. 96 | default: retry.logging_logger. if None, logging is disabled. 97 | :returns: the result of the f function. 98 | """ 99 | args = fargs if fargs else list() 100 | kwargs = fkwargs if fkwargs else dict() 101 | return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, logger) 102 | -------------------------------------------------------------------------------- /retry/compat.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | 5 | try: 6 | from decorator import decorator 7 | except ImportError: 8 | def decorator(caller): 9 | """ Turns caller into a decorator. 10 | Unlike decorator module, function signature is not preserved. 11 | 12 | :param caller: caller(f, *args, **kwargs) 13 | """ 14 | def decor(f): 15 | @functools.wraps(f) 16 | def wrapper(*args, **kwargs): 17 | return caller(f, *args, **kwargs) 18 | return wrapper 19 | return decor 20 | 21 | 22 | try: # Python 2.7+ 23 | from logging import NullHandler 24 | except ImportError: 25 | class NullHandler(logging.Handler): 26 | 27 | def emit(self, record): 28 | pass 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = retry 3 | version = 0.9.3 4 | author = invl 5 | author-email = invlpg@gmail.com 6 | summary = Easy to use retry decorator. 7 | license = Apache License 2.0 8 | description-file = README.rst 9 | home-page = https://github.com/invl/retry 10 | requires-python = >=2.6 11 | classifier = 12 | Development Status :: 4 - Beta 13 | Intended Audience :: Developers 14 | License :: OSI Approved :: Apache Software License 15 | Natural Language :: English 16 | Operating System :: OS Independent 17 | Programming Language :: Python 18 | Programming Language :: Python :: 2.6 19 | Programming Language :: Python :: 2.7 20 | Programming Language :: Python :: 3 21 | Programming Language :: Python :: 3.4 22 | Programming Language :: Python :: Implementation :: PyPy 23 | Topic :: Software Development 24 | 25 | [wheel] 26 | universal = 1 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | setup( 5 | packages=find_packages(), 6 | pbr=True, 7 | setup_requires=['pbr'], 8 | ) 9 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | pbr 3 | pytest 4 | tox 5 | wheel 6 | -------------------------------------------------------------------------------- /tests/test_retry.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import create_autospec 3 | except ImportError: 4 | from mock import create_autospec 5 | 6 | try: 7 | from unittest.mock import MagicMock 8 | except ImportError: 9 | from mock import MagicMock 10 | 11 | import time 12 | 13 | import pytest 14 | 15 | from retry.api import retry_call 16 | from retry.api import retry 17 | 18 | 19 | def test_retry(monkeypatch): 20 | mock_sleep_time = [0] 21 | 22 | def mock_sleep(seconds): 23 | mock_sleep_time[0] += seconds 24 | 25 | monkeypatch.setattr(time, 'sleep', mock_sleep) 26 | 27 | hit = [0] 28 | 29 | tries = 5 30 | delay = 1 31 | backoff = 2 32 | 33 | @retry(tries=tries, delay=delay, backoff=backoff) 34 | def f(): 35 | hit[0] += 1 36 | 1 / 0 37 | 38 | with pytest.raises(ZeroDivisionError): 39 | f() 40 | assert hit[0] == tries 41 | assert mock_sleep_time[0] == sum( 42 | delay * backoff ** i for i in range(tries - 1)) 43 | 44 | 45 | def test_tries_inf(): 46 | hit = [0] 47 | target = 10 48 | 49 | @retry(tries=float('inf')) 50 | def f(): 51 | hit[0] += 1 52 | if hit[0] == target: 53 | return target 54 | else: 55 | raise ValueError 56 | assert f() == target 57 | 58 | 59 | def test_tries_minus1(): 60 | hit = [0] 61 | target = 10 62 | 63 | @retry(tries=-1) 64 | def f(): 65 | hit[0] += 1 66 | if hit[0] == target: 67 | return target 68 | else: 69 | raise ValueError 70 | assert f() == target 71 | 72 | 73 | def test_max_delay(monkeypatch): 74 | mock_sleep_time = [0] 75 | 76 | def mock_sleep(seconds): 77 | mock_sleep_time[0] += seconds 78 | 79 | monkeypatch.setattr(time, 'sleep', mock_sleep) 80 | 81 | hit = [0] 82 | 83 | tries = 5 84 | delay = 1 85 | backoff = 2 86 | max_delay = delay # Never increase delay 87 | 88 | @retry(tries=tries, delay=delay, max_delay=max_delay, backoff=backoff) 89 | def f(): 90 | hit[0] += 1 91 | 1 / 0 92 | 93 | with pytest.raises(ZeroDivisionError): 94 | f() 95 | assert hit[0] == tries 96 | assert mock_sleep_time[0] == delay * (tries - 1) 97 | 98 | 99 | def test_fixed_jitter(monkeypatch): 100 | mock_sleep_time = [0] 101 | 102 | def mock_sleep(seconds): 103 | mock_sleep_time[0] += seconds 104 | 105 | monkeypatch.setattr(time, 'sleep', mock_sleep) 106 | 107 | hit = [0] 108 | 109 | tries = 10 110 | jitter = 1 111 | 112 | @retry(tries=tries, jitter=jitter) 113 | def f(): 114 | hit[0] += 1 115 | 1 / 0 116 | 117 | with pytest.raises(ZeroDivisionError): 118 | f() 119 | assert hit[0] == tries 120 | assert mock_sleep_time[0] == sum(range(tries - 1)) 121 | 122 | 123 | def test_retry_call(): 124 | f_mock = MagicMock(side_effect=RuntimeError) 125 | tries = 2 126 | try: 127 | retry_call(f_mock, exceptions=RuntimeError, tries=tries) 128 | except RuntimeError: 129 | pass 130 | 131 | assert f_mock.call_count == tries 132 | 133 | 134 | def test_retry_call_2(): 135 | side_effect = [RuntimeError, RuntimeError, 3] 136 | f_mock = MagicMock(side_effect=side_effect) 137 | tries = 5 138 | result = None 139 | try: 140 | result = retry_call(f_mock, exceptions=RuntimeError, tries=tries) 141 | except RuntimeError: 142 | pass 143 | 144 | assert result == 3 145 | assert f_mock.call_count == len(side_effect) 146 | 147 | 148 | def test_retry_call_with_args(): 149 | 150 | def f(value=0): 151 | if value < 0: 152 | return value 153 | else: 154 | raise RuntimeError 155 | 156 | return_value = -1 157 | result = None 158 | f_mock = MagicMock(spec=f, return_value=return_value) 159 | try: 160 | result = retry_call(f_mock, fargs=[return_value]) 161 | except RuntimeError: 162 | pass 163 | 164 | assert result == return_value 165 | assert f_mock.call_count == 1 166 | 167 | 168 | def test_retry_call_with_kwargs(): 169 | 170 | def f(value=0): 171 | if value < 0: 172 | return value 173 | else: 174 | raise RuntimeError 175 | 176 | kwargs = {'value': -1} 177 | result = None 178 | f_mock = MagicMock(spec=f, return_value=kwargs['value']) 179 | try: 180 | result = retry_call(f_mock, fkwargs=kwargs) 181 | except RuntimeError: 182 | pass 183 | 184 | assert result == kwargs['value'] 185 | assert f_mock.call_count == 1 186 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = flake8, py26, py27, py34, py35, pypy 8 | 9 | [flake8] 10 | max-line-length = 120 11 | exclude = *.cfg,*.egg,*.ini,*.log,*tests*,*.txt,*.xml,.tox,.venv,AUTHORS,build,ChangeLog,dist,doc,test-requirements.txt,src 12 | format = pylint 13 | 14 | [testenv] 15 | commands = py.test 16 | deps= 17 | -rrequirements.txt 18 | -rtest-requirements.txt 19 | 20 | [testenv:flake8] 21 | basepython = python3.4 22 | commands = flake8 23 | deps = flake8 24 | --------------------------------------------------------------------------------