├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── setup.py ├── tests ├── __init__.py └── test_timeout_decorator.py ├── timeout_decorator ├── __init__.py └── timeout_decorator.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.swp 3 | *~ 4 | venv 5 | .env 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Packages 11 | *.egg 12 | *.egg-info 13 | dist 14 | build 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | .cache 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | .ropeproject/ 43 | .vscode/ 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - '2.7' 5 | - '3.6' 6 | - '3.7' 7 | - '3.8' 8 | install: 9 | - pip install python-coveralls tox tox-travis 10 | script: tox --recreate 11 | after_success: 12 | - pip install -e . 13 | - py.test --cov=timeout_decorator --cov-report=term-missing tests 14 | - coveralls 15 | deploy: 16 | provider: pypi 17 | user: png 18 | password: 19 | secure: ZXoq3kgfu+IICjhhmQZr0s0xE6bvWzH04GjdE/VL4BxdDdGI4fHEwudGEjzLXJbt2d09vNOO67Nqam+MwPWtq+WZEP69g/Fhyy4kbkuUl/CMeqashQzU/N+3lwv97Y2qvzTUwDnSoz4zyBFu67SSrovKruFsYaiH00bwvWcvLa0= 20 | on: 21 | python: 2.7 22 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.3.1 5 | ----- 6 | - Fixed issue with PicklingError causes the timeout to never be reached. 7 | 8 | 0.3.0 9 | ----- 10 | 11 | - Added optional threading support via python multiprocessing (bubenkoff) 12 | - Switched to pytest test runner (bubenkoff) 13 | 14 | 15 | 0.2.1 16 | ----- 17 | 18 | - Initial public release 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2014 Patrick Ng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # create virtual environment 2 | venv: 3 | virtualenv venv 4 | 5 | # install all needed for development 6 | develop: venv 7 | venv/bin/pip install -e . -r requirements-testing.txt tox 8 | 9 | # clean the development envrironment 10 | clean: 11 | -rm -rf venv 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Timeout decorator 2 | ================= 3 | 4 | |Build Status| |Pypi Status| |Coveralls Status| 5 | 6 | Installation 7 | ------------ 8 | 9 | From source code: 10 | 11 | :: 12 | 13 | python setup.py install 14 | 15 | From pypi: 16 | 17 | :: 18 | 19 | pip install timeout-decorator 20 | 21 | Usage 22 | ----- 23 | 24 | :: 25 | 26 | import time 27 | import timeout_decorator 28 | 29 | @timeout_decorator.timeout(5) 30 | def mytest(): 31 | print("Start") 32 | for i in range(1,10): 33 | time.sleep(1) 34 | print("{} seconds have passed".format(i)) 35 | 36 | if __name__ == '__main__': 37 | mytest() 38 | 39 | Specify an alternate exception to raise on timeout: 40 | 41 | :: 42 | 43 | import time 44 | import timeout_decorator 45 | 46 | @timeout_decorator.timeout(5, timeout_exception=StopIteration) 47 | def mytest(): 48 | print("Start") 49 | for i in range(1,10): 50 | time.sleep(1) 51 | print("{} seconds have passed".format(i)) 52 | 53 | if __name__ == '__main__': 54 | mytest() 55 | 56 | Multithreading 57 | -------------- 58 | 59 | By default, timeout-decorator uses signals to limit the execution time 60 | of the given function. This appoach does not work if your function is 61 | executed not in a main thread (for example if it's a worker thread of 62 | the web application). There is alternative timeout strategy for this 63 | case - by using multiprocessing. To use it, just pass 64 | ``use_signals=False`` to the timeout decorator function: 65 | 66 | :: 67 | 68 | import time 69 | import timeout_decorator 70 | 71 | @timeout_decorator.timeout(5, use_signals=False) 72 | def mytest(): 73 | print "Start" 74 | for i in range(1,10): 75 | time.sleep(1) 76 | print("{} seconds have passed".format(i)) 77 | 78 | if __name__ == '__main__': 79 | mytest() 80 | 81 | .. warning:: 82 | Make sure that in case of multiprocessing strategy for timeout, your function does not return objects which cannot 83 | be pickled, otherwise it will fail at marshalling it between master and child processes. 84 | 85 | 86 | Acknowledgement 87 | --------------- 88 | 89 | Derived from 90 | http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/ 91 | and https://code.google.com/p/verse-quiz/source/browse/trunk/timeout.py 92 | 93 | Contribute 94 | ---------- 95 | 96 | I would love for you to fork and send me pull request for this project. 97 | Please contribute. 98 | 99 | License 100 | ------- 101 | 102 | This software is licensed under the `MIT license `_ 103 | 104 | See `License file `_ 105 | 106 | .. |Build Status| image:: https://travis-ci.org/pnpnpn/timeout-decorator.svg?branch=master 107 | :target: https://travis-ci.org/pnpnpn/timeout-decorator 108 | .. |Pypi Status| image:: https://badge.fury.io/py/timeout-decorator.svg 109 | :target: https://badge.fury.io/py/timeout-decorator 110 | .. |Coveralls Status| image:: https://coveralls.io/repos/pnpnpn/timeout-decorator/badge.png?branch=master 111 | :target: https://coveralls.io/r/pnpnpn/timeout-decorator 112 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setuptools entry point.""" 2 | import codecs 3 | import os 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | 11 | CLASSIFIERS = [ 12 | 'Development Status :: 4 - Beta', 13 | 'Intended Audience :: Developers', 14 | 'License :: OSI Approved :: MIT License', 15 | 'Natural Language :: English', 16 | 'Operating System :: OS Independent', 17 | 'Programming Language :: Python', 18 | 'Topic :: Software Development :: Libraries :: Python Modules' 19 | ] 20 | 21 | dirname = os.path.dirname(__file__) 22 | 23 | long_description = ( 24 | codecs.open(os.path.join(dirname, 'README.rst'), encoding='utf-8').read() + '\n' + 25 | codecs.open(os.path.join(dirname, 'CHANGES.rst'), encoding='utf-8').read() 26 | ) 27 | 28 | setup( 29 | name='timeout-decorator', 30 | version='0.5.0', 31 | description='Timeout decorator', 32 | long_description=long_description, 33 | author='Patrick Ng', 34 | author_email='pn.appdev@gmail.com', 35 | url='https://github.com/pnpnpn/timeout-decorator', 36 | packages=['timeout_decorator'], 37 | install_requires=[], 38 | classifiers=CLASSIFIERS) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnpnpn/timeout-decorator/9fbc3ef5b6f8f8cba2eb7ba795813d6ec543e265/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_timeout_decorator.py: -------------------------------------------------------------------------------- 1 | """Timeout decorator tests.""" 2 | import time 3 | 4 | import pytest 5 | 6 | from timeout_decorator import timeout, TimeoutError 7 | 8 | 9 | @pytest.fixture(params=[False, True]) 10 | def use_signals(request): 11 | """Use signals for timing out or not.""" 12 | return request.param 13 | 14 | 15 | def test_timeout_decorator_arg(use_signals): 16 | @timeout(1, use_signals=use_signals) 17 | def f(): 18 | time.sleep(2) 19 | with pytest.raises(TimeoutError): 20 | f() 21 | 22 | 23 | def test_timeout_class_method(use_signals): 24 | class c(): 25 | @timeout(1, use_signals=use_signals) 26 | def f(self): 27 | time.sleep(2) 28 | with pytest.raises(TimeoutError): 29 | c().f() 30 | 31 | 32 | def test_timeout_kwargs(use_signals): 33 | @timeout(3, use_signals=use_signals) 34 | def f(): 35 | time.sleep(2) 36 | with pytest.raises(TimeoutError): 37 | f(timeout=1) 38 | 39 | 40 | def test_timeout_alternate_exception(use_signals): 41 | @timeout(3, use_signals=use_signals, timeout_exception=StopIteration) 42 | def f(): 43 | time.sleep(2) 44 | with pytest.raises(StopIteration): 45 | f(timeout=1) 46 | 47 | 48 | def test_timeout_kwargs_with_initial_timeout_none(use_signals): 49 | @timeout(use_signals=use_signals) 50 | def f(): 51 | time.sleep(2) 52 | with pytest.raises(TimeoutError): 53 | f(timeout=1) 54 | 55 | 56 | def test_timeout_no_seconds(use_signals): 57 | @timeout(use_signals=use_signals) 58 | def f(): 59 | time.sleep(0.1) 60 | f() 61 | 62 | 63 | def test_timeout_partial_seconds(use_signals): 64 | @timeout(0.2, use_signals=use_signals) 65 | def f(): 66 | time.sleep(0.5) 67 | with pytest.raises(TimeoutError): 68 | f() 69 | 70 | 71 | def test_timeout_ok(use_signals): 72 | @timeout(seconds=2, use_signals=use_signals) 73 | def f(): 74 | time.sleep(1) 75 | f() 76 | 77 | 78 | def test_function_name(use_signals): 79 | @timeout(seconds=2, use_signals=use_signals) 80 | def func_name(): 81 | pass 82 | 83 | assert func_name.__name__ == 'func_name' 84 | 85 | 86 | def test_timeout_pickle_error(): 87 | """Test that when a pickle error occurs a timeout error is raised.""" 88 | @timeout(seconds=1, use_signals=False) 89 | def f(): 90 | time.sleep(0.1) 91 | 92 | class Test(object): 93 | pass 94 | return Test() 95 | with pytest.raises(TimeoutError): 96 | f() 97 | 98 | 99 | def test_timeout_custom_exception_message(): 100 | @timeout(seconds=1, exception_message="Custom fail message") 101 | def f(): 102 | time.sleep(2) 103 | with pytest.raises(TimeoutError, match="Custom fail message"): 104 | f() 105 | 106 | 107 | def test_timeout_custom_exception_with_message(): 108 | @timeout(seconds=1, timeout_exception=RuntimeError, exception_message="Custom fail message") 109 | def f(): 110 | time.sleep(2) 111 | with pytest.raises(RuntimeError, match="Custom fail message"): 112 | f() 113 | 114 | 115 | def test_timeout_default_exception_message(): 116 | @timeout(seconds=1) 117 | def f(): 118 | time.sleep(2) 119 | with pytest.raises(TimeoutError, match="Timed Out"): 120 | f() 121 | -------------------------------------------------------------------------------- /timeout_decorator/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .timeout_decorator import timeout 4 | from .timeout_decorator import TimeoutError 5 | 6 | __title__ = 'timeout_decorator' 7 | __version__ = '0.5.0' 8 | -------------------------------------------------------------------------------- /timeout_decorator/timeout_decorator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Timeout decorator. 3 | 4 | :copyright: (c) 2012-2013 by PN. 5 | :license: MIT, see LICENSE for more details. 6 | """ 7 | 8 | from __future__ import print_function 9 | from __future__ import unicode_literals 10 | from __future__ import division 11 | 12 | import sys 13 | import time 14 | import multiprocessing 15 | import signal 16 | from functools import wraps 17 | 18 | ############################################################ 19 | # Timeout 20 | ############################################################ 21 | 22 | # http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/ 23 | # Used work of Stephen "Zero" Chappell 24 | # in https://code.google.com/p/verse-quiz/source/browse/trunk/timeout.py 25 | 26 | 27 | class TimeoutError(AssertionError): 28 | 29 | """Thrown when a timeout occurs in the `timeout` context manager.""" 30 | 31 | def __init__(self, value="Timed Out"): 32 | self.value = value 33 | 34 | def __str__(self): 35 | return repr(self.value) 36 | 37 | 38 | def _raise_exception(exception, exception_message): 39 | """ This function checks if a exception message is given. 40 | 41 | If there is no exception message, the default behaviour is maintained. 42 | If there is an exception message, the message is passed to the exception with the 'value' keyword. 43 | """ 44 | if exception_message is None: 45 | raise exception() 46 | else: 47 | raise exception(exception_message) 48 | 49 | 50 | def timeout(seconds=None, use_signals=True, timeout_exception=TimeoutError, exception_message=None): 51 | """Add a timeout parameter to a function and return it. 52 | 53 | :param seconds: optional time limit in seconds or fractions of a second. If None is passed, no timeout is applied. 54 | This adds some flexibility to the usage: you can disable timing out depending on the settings. 55 | :type seconds: float 56 | :param use_signals: flag indicating whether signals should be used for timing function out or the multiprocessing 57 | When using multiprocessing, timeout granularity is limited to 10ths of a second. 58 | :type use_signals: bool 59 | 60 | :raises: TimeoutError if time limit is reached 61 | 62 | It is illegal to pass anything other than a function as the first 63 | parameter. The function is wrapped and returned to the caller. 64 | """ 65 | def decorate(function): 66 | 67 | if use_signals: 68 | def handler(signum, frame): 69 | _raise_exception(timeout_exception, exception_message) 70 | 71 | @wraps(function) 72 | def new_function(*args, **kwargs): 73 | new_seconds = kwargs.pop('timeout', seconds) 74 | if new_seconds: 75 | old = signal.signal(signal.SIGALRM, handler) 76 | signal.setitimer(signal.ITIMER_REAL, new_seconds) 77 | 78 | if not seconds: 79 | return function(*args, **kwargs) 80 | 81 | try: 82 | return function(*args, **kwargs) 83 | finally: 84 | if new_seconds: 85 | signal.setitimer(signal.ITIMER_REAL, 0) 86 | signal.signal(signal.SIGALRM, old) 87 | return new_function 88 | else: 89 | @wraps(function) 90 | def new_function(*args, **kwargs): 91 | timeout_wrapper = _Timeout(function, timeout_exception, exception_message, seconds) 92 | return timeout_wrapper(*args, **kwargs) 93 | return new_function 94 | 95 | return decorate 96 | 97 | 98 | def _target(queue, function, *args, **kwargs): 99 | """Run a function with arguments and return output via a queue. 100 | 101 | This is a helper function for the Process created in _Timeout. It runs 102 | the function with positional arguments and keyword arguments and then 103 | returns the function's output by way of a queue. If an exception gets 104 | raised, it is returned to _Timeout to be raised by the value property. 105 | """ 106 | try: 107 | queue.put((True, function(*args, **kwargs))) 108 | except: 109 | queue.put((False, sys.exc_info()[1])) 110 | 111 | 112 | class _Timeout(object): 113 | 114 | """Wrap a function and add a timeout (limit) attribute to it. 115 | 116 | Instances of this class are automatically generated by the add_timeout 117 | function defined above. Wrapping a function allows asynchronous calls 118 | to be made and termination of execution after a timeout has passed. 119 | """ 120 | 121 | def __init__(self, function, timeout_exception, exception_message, limit): 122 | """Initialize instance in preparation for being called.""" 123 | self.__limit = limit 124 | self.__function = function 125 | self.__timeout_exception = timeout_exception 126 | self.__exception_message = exception_message 127 | self.__name__ = function.__name__ 128 | self.__doc__ = function.__doc__ 129 | self.__timeout = time.time() 130 | self.__process = multiprocessing.Process() 131 | self.__queue = multiprocessing.Queue() 132 | 133 | def __call__(self, *args, **kwargs): 134 | """Execute the embedded function object asynchronously. 135 | 136 | The function given to the constructor is transparently called and 137 | requires that "ready" be intermittently polled. If and when it is 138 | True, the "value" property may then be checked for returned data. 139 | """ 140 | self.__limit = kwargs.pop('timeout', self.__limit) 141 | self.__queue = multiprocessing.Queue(1) 142 | args = (self.__queue, self.__function) + args 143 | self.__process = multiprocessing.Process(target=_target, 144 | args=args, 145 | kwargs=kwargs) 146 | self.__process.daemon = True 147 | self.__process.start() 148 | if self.__limit is not None: 149 | self.__timeout = self.__limit + time.time() 150 | while not self.ready: 151 | time.sleep(0.01) 152 | return self.value 153 | 154 | def cancel(self): 155 | """Terminate any possible execution of the embedded function.""" 156 | if self.__process.is_alive(): 157 | self.__process.terminate() 158 | 159 | _raise_exception(self.__timeout_exception, self.__exception_message) 160 | 161 | @property 162 | def ready(self): 163 | """Read-only property indicating status of "value" property.""" 164 | if self.__limit and self.__timeout < time.time(): 165 | self.cancel() 166 | return self.__queue.full() and not self.__queue.empty() 167 | 168 | @property 169 | def value(self): 170 | """Read-only property containing data returned from function.""" 171 | if self.ready is True: 172 | flag, load = self.__queue.get() 173 | if flag: 174 | return load 175 | raise load 176 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | distshare={homedir}/.tox/distshare 3 | envlist=py{27,36,37,38} 4 | skip_missing_interpreters=true 5 | indexserver= 6 | pypi = https://pypi.python.org/simple 7 | 8 | [testenv] 9 | commands= 10 | py.test timeout_decorator tests 11 | deps = 12 | pytest 13 | pytest-pep8 14 | 15 | [pytest] 16 | addopts = -vvl 17 | pep8maxlinelength=120 18 | --------------------------------------------------------------------------------