diff options
author | Anatoly Bubenkov <bubenkoff@gmail.com> | 2015-03-15 13:41:13 +0100 |
---|---|---|
committer | Anatoly Bubenkov <bubenkoff@gmail.com> | 2015-03-16 17:23:00 +0100 |
commit | b1edeeaa1d68b5f84becdbd31cfef0ac690bd2a7 (patch) | |
tree | dd1761f3b6ca79f5226b210025d2f8b68896aaad | |
parent | 2304ff00ac08985ead06d0225591fa6508772ab2 (diff) | |
download | timeout-decorator-b1edeeaa1d68b5f84becdbd31cfef0ac690bd2a7.tar.gz |
Added optional threading support via python multiprocessing. Switched to pytest test runner
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .travis.yml | 13 | ||||
-rw-r--r-- | CHANGES.rst | 14 | ||||
-rw-r--r-- | LICENSE.txt | 22 | ||||
-rw-r--r-- | MANIFEST.in | 2 | ||||
-rw-r--r-- | Makefile | 11 | ||||
-rw-r--r-- | README.md | 62 | ||||
-rw-r--r-- | README.rst | 94 | ||||
-rw-r--r-- | requirements-testing.txt | 2 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | setup.py | 36 | ||||
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/test_timeout_decorator.py | 47 | ||||
-rw-r--r-- | timeout_decorator/__init__.py | 2 | ||||
-rw-r--r-- | timeout_decorator/timeout_decorator.py | 155 | ||||
-rw-r--r-- | tox.ini | 14 |
16 files changed, 344 insertions, 132 deletions
@@ -2,6 +2,7 @@ *.swp *~ venv +.env # C extensions *.so diff --git a/.travis.yml b/.travis.yml index 67fd481..a60894e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,13 @@ language: python python: -- '2.6' - '2.7' -- '3.2' -- '3.3' -- '3.4' -install: pip install -r requirements.txt -script: nosetests +install: +- pip install python-coveralls virtualenv tox +script: tox +after_success: +- pip install -r requirements-testing.txt -e . +- py.test --cov=timeout_decorator --cov-report=term-missing tests +- coveralls deploy: provider: pypi user: png diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..dd67619 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,14 @@ +Changelog +========= + +0.3.0 +----- + +- Added optional threading support via python multiprocessing (bubenkoff) +- Switched to pytest test runner (bubenkoff) + + +0.2.1 +----- + +- Initial public release diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..671c599 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2012-2014 Patrick Ng + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index b9560c9..bb37a27 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include README.md LICENSE tests/*.py +include *.rst diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..017607e --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +# create virtual environment +venv: + virtualenv venv + +# install all needed for development +develop: venv + venv/bin/pip install -e . -r requirements-testing.txt tox + +# clean the development envrironment +clean: + -rm -rf venv diff --git a/README.md b/README.md deleted file mode 100644 index 5d00325..0000000 --- a/README.md +++ /dev/null @@ -1,62 +0,0 @@ -[![Build Status](https://travis-ci.org/pnpnpn/timeout-decorator.svg?branch=master)](https://travis-ci.org/pnpnpn/timeout-decorator) - - -Installation ------------- -From source code: - - python setup.py install - -From pypi: - - pip install timeout-decorator - -Usage ------ - - - import time - import timeout_decorator - - @timeout_decorator.timeout(5) - def mytest(): - print "Start" - for i in range(1,10): - time.sleep(1) - print "%d seconds have passed" % i - - if __name__ == '__main__': - mytest() - - -Acknowledgement --------------------- -Derived from http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/ - -Contribute ------------- -I would love for you to fork and send me pull request for this project. Please contribute. - -License ---------- -The MIT License (MIT) - -Copyright (c) 2012-2014 Patrick Ng - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..5573180 --- /dev/null +++ b/README.rst @@ -0,0 +1,94 @@ +Timeout decorator +================= + +|Build Status| |Pypi Status| |Coveralls Status| + +Installation +------------ + +From source code: + +:: + + python setup.py install + +From pypi: + +:: + + pip install timeout-decorator + +Usage +----- + +:: + + import time + import timeout_decorator + + @timeout_decorator.timeout(5) + def mytest(): + print "Start" + for i in range(1,10): + time.sleep(1) + print "%d seconds have passed" % i + + if __name__ == '__main__': + mytest() + +Multithreading +-------------- + +By default, timeout-decorator uses signals to limit the execution time +of the given function. This appoach does not work if your function is +executed not in a main thread (for example if it's a worker thread of +the web application). There is alternative timeout strategy for this +case - by using multiprocessing. To use it, just pass +``use_signals=False`` to the timeout decorator function: + +:: + + import time + import timeout_decorator + + @timeout_decorator.timeout(5, use_signals=False) + def mytest(): + print "Start" + for i in range(1,10): + time.sleep(1) + print "%d seconds have passed" % i + + if __name__ == '__main__': + mytest() + +.. warning:: + Make sure that in case of multiprocessing strategy for timeout, your function does not return objects which cannot + be pickled, otherwise it will fail at marshalling it between master and child processes. + + +Acknowledgement +--------------- + +Derived from +http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/ +and https://code.google.com/p/verse-quiz/source/browse/trunk/timeout.py + +Contribute +---------- + +I would love for you to fork and send me pull request for this project. +Please contribute. + +License +------- + +This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_ + +See `License file <https://github.com/pnpnpn/timeout-decorator/blob/master/LICENSE.txt>`_ + +.. |Build Status| image:: https://travis-ci.org/pnpnpn/timeout-decorator.svg?branch=master + :target: https://travis-ci.org/pnpnpn/timeout-decorator +.. |Pypi Status| image:: https://pypip.in/v/timeout-decorator/badge.png + :target: https://crate.io/packages/timeout-decorator/ +.. |Coveralls Status| image:: https://coveralls.io/repos/pnpnpn/timeout-decorator/badge.png?branch=master + :target: https://coveralls.io/r/pnpnpn/timeout-decorator diff --git a/requirements-testing.txt b/requirements-testing.txt new file mode 100644 index 0000000..644a741 --- /dev/null +++ b/requirements-testing.txt @@ -0,0 +1,2 @@ +pytest==2.6.4 +pytest-pep8==1.0.6 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dc2fd71..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -nose==1.3.4 @@ -1,5 +1,6 @@ -# -*- coding: utf-8 -*- -import sys +"""Setuptools entry point.""" +import codecs +import os try: from setuptools import setup @@ -7,7 +8,7 @@ except ImportError: from distutils.core import setup -CLASSIFIERS=[ +CLASSIFIERS = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', @@ -15,18 +16,23 @@ CLASSIFIERS=[ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules' - ] +] -setup( - name='timeout-decorator', - version='0.2.1', - description='Timeout decorator', - long_description = open('README.md').read(), - author='PN', - author_email='pn.appdev@gmail.com', - url='https://github.com/pnpnpn/timeout-decorator', - packages=['timeout_decorator'], - install_requires=[], - classifiers=CLASSIFIERS) +dirname = os.path.dirname(__file__) +long_description = ( + codecs.open(os.path.join(dirname, 'README.rst'), encoding='utf-8').read() + '\n' + + codecs.open(os.path.join(dirname, 'CHANGES.rst'), encoding='utf-8').read() +) +setup( + name='timeout-decorator', + version='0.3.0', + description='Timeout decorator', + long_description=long_description, + author='Patrick Ng', + author_email='pn.appdev@gmail.com', + url='https://github.com/pnpnpn/timeout-decorator', + packages=['timeout_decorator'], + install_requires=[], + classifiers=CLASSIFIERS) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/test_timeout_decorator.py b/tests/test_timeout_decorator.py index c40c82e..12d5389 100644 --- a/tests/test_timeout_decorator.py +++ b/tests/test_timeout_decorator.py @@ -1,46 +1,49 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - +"""Timeout decorator tests.""" import time -from nose.tools import raises +import pytest from timeout_decorator import timeout, TimeoutError -@raises(TimeoutError) -def test_timeout_decorator_arg(): - @timeout(1) +@pytest.fixture(params=[False, True]) +def use_signals(request): + """Use signals for timing out or not.""" + return request.param + + +def test_timeout_decorator_arg(use_signals): + @timeout(1, use_signals=use_signals) def f(): time.sleep(2) - f() + with pytest.raises(TimeoutError): + f() -@raises(TimeoutError) -def test_timeout_kwargs(): - @timeout() - def f(timeout): +def test_timeout_kwargs(use_signals): + @timeout(3, use_signals=use_signals) + def f(): time.sleep(2) - f(timeout=1) + with pytest.raises(TimeoutError): + f(timeout=1) -@raises(ValueError) -def test_timeout_no_seconds(): - @timeout() - def f(timeout): - time.sleep(2) +def test_timeout_no_seconds(use_signals): + @timeout(use_signals=use_signals) + def f(): + time.sleep(0.1) f() -def test_timeout_ok(): - @timeout(seconds=2) +def test_timeout_ok(use_signals): + @timeout(seconds=2, use_signals=use_signals) def f(): time.sleep(1) f() -def test_function_name(): - @timeout(seconds=2) +def test_function_name(use_signals): + @timeout(seconds=2, use_signals=use_signals) def func_name(): pass diff --git a/timeout_decorator/__init__.py b/timeout_decorator/__init__.py index 2e5cb75..7a0181b 100644 --- a/timeout_decorator/__init__.py +++ b/timeout_decorator/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from .timeout_decorator import * +from .timeout_decorator import timeout from .timeout_decorator import TimeoutError __title__ = 'timeout_decorator' diff --git a/timeout_decorator/timeout_decorator.py b/timeout_decorator/timeout_decorator.py index f4bb024..014abc0 100644 --- a/timeout_decorator/timeout_decorator.py +++ b/timeout_decorator/timeout_decorator.py @@ -1,7 +1,6 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ +Timeout decorator. + :copyright: (c) 2012-2013 by PN. :license: MIT, see LICENSE for more details. """ @@ -10,6 +9,9 @@ from __future__ import print_function from __future__ import unicode_literals from __future__ import division +import sys +import time +import multiprocessing import signal from functools import wraps @@ -17,10 +19,15 @@ from functools import wraps # Timeout ############################################################ -#http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/ +# http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/ +# Used work of Stephen "Zero" Chappell <Noctis.Skytower@gmail.com> +# in https://code.google.com/p/verse-quiz/source/browse/trunk/timeout.py class TimeoutError(AssertionError): + + """Thrown when a timeout occurs in the `timeout` context manager.""" + def __init__(self, value="Timed Out"): self.value = value @@ -28,25 +35,125 @@ class TimeoutError(AssertionError): return repr(self.value) -def timeout(seconds=None): - def decorate(f): - def handler(signum, frame): - raise TimeoutError() +def timeout(seconds=None, use_signals=True): + """Add a timeout parameter to a function and return it. + + :param seconds: optional time limit in seconds. If None is passed, no timeout is applied. + This adds some flexibility to the usage: you can disable timing out depending on the settings. + :type seconds: int + :param use_signals: flag indicating whether signals should be used for timing function out or the multiprocessing + :type use_signals: bool + + :raises: TimeoutError if time limit is reached + + It is illegal to pass anything other than a function as the first + parameter. The function is wrapped and returned to the caller. + """ + def decorate(function): + + if not seconds: + return function + + if use_signals: + def handler(signum, frame): + raise TimeoutError() + + @wraps(function) + def new_function(*args, **kwargs): + new_seconds = kwargs.pop('timeout', seconds) + if new_seconds: + old = signal.signal(signal.SIGALRM, handler) + signal.alarm(new_seconds) + try: + return function(*args, **kwargs) + finally: + if new_seconds: + signal.alarm(0) + signal.signal(signal.SIGALRM, old) + return new_function + else: + return _Timeout(function, seconds) - @wraps(f) - def new_f(*args, **kwargs): - old = signal.signal(signal.SIGALRM, handler) - - new_seconds = kwargs['timeout'] if 'timeout' in kwargs else seconds - if new_seconds is None: - raise ValueError("You must provide a timeout value") - - signal.alarm(new_seconds) - try: - result = f(*args, **kwargs) - finally: - signal.alarm(0) - signal.signal(signal.SIGALRM, old) - return result - return new_f return decorate + + +def _target(queue, function, *args, **kwargs): + """Run a function with arguments and return output via a queue. + + This is a helper function for the Process created in _Timeout. It runs + the function with positional arguments and keyword arguments and then + returns the function's output by way of a queue. If an exception gets + raised, it is returned to _Timeout to be raised by the value property. + """ + try: + queue.put((True, function(*args, **kwargs))) + except: + queue.put((False, sys.exc_info()[1])) + + +class _Timeout: + + """Wrap a function and add a timeout (limit) attribute to it. + + Instances of this class are automatically generated by the add_timeout + function defined above. Wrapping a function allows asynchronous calls + to be made and termination of execution after a timeout has passed. + """ + + def __init__(self, function, limit): + """Initialize instance in preparation for being called.""" + self.__limit = limit + self.__function = function + self.__name__ = function.__name__ + self.__doc__ = function.__doc__ + self.__timeout = time.time() + self.__process = multiprocessing.Process() + self.__queue = multiprocessing.Queue() + + def __call__(self, *args, **kwargs): + """Execute the embedded function object asynchronously. + + The function given to the constructor is transparently called and + requires that "ready" be intermittently polled. If and when it is + True, the "value" property may then be checked for returned data. + """ + self.__limit = kwargs.pop('timeout', self.__limit) + self.cancel() + self.__queue = multiprocessing.Queue(1) + args = (self.__queue, self.__function) + args + self.__process = multiprocessing.Process(target=_target, + args=args, + kwargs=kwargs) + self.__process.daemon = True + self.__process.start() + self.__timeout = self.__limit + time.time() + while not self.ready: + time.sleep(0.01) + return self.value + + def cancel(self): + """Terminate any possible execution of the embedded function.""" + if self.__process.is_alive(): + self.__process.terminate() + raise TimeoutError() + + @property + def ready(self): + """Read-only property indicating status of "value" property.""" + if self.__queue.full(): + return True + elif not self.__queue.empty(): + return True + elif self.__timeout < time.time(): + self.cancel() + else: + return False + + @property + def value(self): + """Read-only property containing data returned from function.""" + if self.ready is True: + flag, load = self.__queue.get() + if flag: + return load + raise load @@ -0,0 +1,14 @@ +[tox] +distshare={homedir}/.tox/distshare +envlist=py26,py27,py32,py33,py34 +skip_missing_interpreters=true +indexserver= + pypi = https://pypi.python.org/simple + +[testenv] +commands= py.test timeout_decorator tests --pep8 +deps = -r{toxinidir}/requirements-testing.txt + +[pytest] +addopts = -vvl +pep8maxlinelength=120 |