aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick <pnpnpn@users.noreply.github.com>2015-03-17 18:36:49 -0400
committerPatrick <pnpnpn@users.noreply.github.com>2015-03-17 18:36:49 -0400
commit3d66d09b20c6de36f35ec582b9c7716ba83eb641 (patch)
treedd1761f3b6ca79f5226b210025d2f8b68896aaad
parent2304ff00ac08985ead06d0225591fa6508772ab2 (diff)
parentb1edeeaa1d68b5f84becdbd31cfef0ac690bd2a7 (diff)
downloadtimeout-decorator-3d66d09b20c6de36f35ec582b9c7716ba83eb641.tar.gz
Merge pull request #16 from bubenkoff/multithreading-support
Added optional threading support via python multiprocessing.
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml13
-rw-r--r--CHANGES.rst14
-rw-r--r--LICENSE.txt22
-rw-r--r--MANIFEST.in2
-rw-r--r--Makefile11
-rw-r--r--README.md62
-rw-r--r--README.rst94
-rw-r--r--requirements-testing.txt2
-rw-r--r--requirements.txt1
-rw-r--r--setup.py36
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/test_timeout_decorator.py47
-rw-r--r--timeout_decorator/__init__.py2
-rw-r--r--timeout_decorator/timeout_decorator.py155
-rw-r--r--tox.ini14
16 files changed, 344 insertions, 132 deletions
diff --git a/.gitignore b/.gitignore
index 20680a8..280c69c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/setup.py b/setup.py
index dbc7ebc..f5d59ad 100644
--- a/setup.py
+++ b/setup.py
@@ -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
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..3cb3a24
--- /dev/null
+++ b/tox.ini
@@ -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