diff options
author | frankfeng <frankfeng@google.com> | 2022-04-25 09:39:17 -0700 |
---|---|---|
committer | frankfeng <frankfeng@google.com> | 2022-04-25 09:43:42 -0700 |
commit | a77f26f116d52effd2a4600f6680a9f7b94fe263 (patch) | |
tree | e769742eb931143f8f2c24d94e6164c8d43914b7 | |
parent | ffe302e4231ab8882d9a8ea68c86ed2a04a28fd8 (diff) | |
parent | 9fbc3ef5b6f8f8cba2eb7ba795813d6ec543e265 (diff) | |
download | timeout-decorator-a77f26f116d52effd2a4600f6680a9f7b94fe263.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' into timeout-decoratorplatform-tools-33.0.2
Bug: 228457005
Bug: 225958696
Test: TH
Change-Id: I96c55c324f786a693281ac261db04fcd91284cd7
-rw-r--r-- | .gitignore | 43 | ||||
-rw-r--r-- | .travis.yml | 21 | ||||
-rw-r--r-- | CHANGES.rst | 18 | ||||
l--------- | LICENSE | 1 | ||||
-rw-r--r-- | LICENSE.txt | 22 | ||||
-rw-r--r-- | MANIFEST.in | 1 | ||||
-rw-r--r-- | METADATA | 16 | ||||
-rw-r--r-- | MODULE_LICENSE_MIT | 0 | ||||
-rw-r--r-- | Makefile | 11 | ||||
-rw-r--r-- | NOTICE | 22 | ||||
-rw-r--r-- | OWNERS | 8 | ||||
-rw-r--r-- | README.rst | 111 | ||||
-rw-r--r-- | setup.py | 38 | ||||
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/test_timeout_decorator.py | 120 | ||||
-rw-r--r-- | timeout_decorator/__init__.py | 7 | ||||
-rw-r--r-- | timeout_decorator/timeout_decorator.py | 175 | ||||
-rw-r--r-- | tox.ini | 17 |
18 files changed, 631 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b7661f --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +*.py[cod] +*.swp +*~ +venv +.env + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +.cache +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +.ropeproject/ +.vscode/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..02adf35 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: python +sudo: false +python: +- '2.7' +- '3.6' +- '3.7' +- '3.8' +install: +- pip install python-coveralls tox tox-travis +script: tox --recreate +after_success: +- pip install -e . +- py.test --cov=timeout_decorator --cov-report=term-missing tests +- coveralls +deploy: + provider: pypi + user: png + password: + secure: ZXoq3kgfu+IICjhhmQZr0s0xE6bvWzH04GjdE/VL4BxdDdGI4fHEwudGEjzLXJbt2d09vNOO67Nqam+MwPWtq+WZEP69g/Fhyy4kbkuUl/CMeqashQzU/N+3lwv97Y2qvzTUwDnSoz4zyBFu67SSrovKruFsYaiH00bwvWcvLa0= + on: + python: 2.7 diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..1fd3175 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,18 @@ +Changelog +========= + +0.3.1 +----- +- Fixed issue with PicklingError causes the timeout to never be reached. + +0.3.0 +----- + +- Added optional threading support via python multiprocessing (bubenkoff) +- Switched to pytest test runner (bubenkoff) + + +0.2.1 +----- + +- Initial public release @@ -0,0 +1 @@ +LICENSE.txt
\ No newline at end of file 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 new file mode 100644 index 0000000..bb37a27 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include *.rst diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..5323ad6 --- /dev/null +++ b/METADATA @@ -0,0 +1,16 @@ +name: "timeout-decorator" +description: + "timeout-decorator" +third_party { + url { + type: HOMEPAGE + value: "https://github.com/pnpnpn/timeout-decorator" + } + url { + type: GIT + value: "https://github.com/pnpnpn/timeout-decorator" + } + version: "9fbc3ef5b6f8f8cba2eb7ba795813d6ec543e265" + last_upgrade_date { year: 2022 month: 4 day: 25 } + license_type: NOTICE +} diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_MIT 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 @@ -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. @@ -0,0 +1,8 @@ +# Android side engprod team +jdesprez@google.com +frankfeng@google.com +murj@google.com + +# Mobly team - use for mobly bugs +angli@google.com +lancefluger@google.com diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..2dcea7b --- /dev/null +++ b/README.rst @@ -0,0 +1,111 @@ +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("{} seconds have passed".format(i)) + + if __name__ == '__main__': + mytest() + +Specify an alternate exception to raise on timeout: + +:: + + import time + import timeout_decorator + + @timeout_decorator.timeout(5, timeout_exception=StopIteration) + def mytest(): + print("Start") + for i in range(1,10): + time.sleep(1) + print("{} seconds have passed".format(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("{} seconds have passed".format(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://badge.fury.io/py/timeout-decorator.svg + :target: https://badge.fury.io/py/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/setup.py b/setup.py new file mode 100644 index 0000000..32771a8 --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +"""Setuptools entry point.""" +import codecs +import os + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + + +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules' +] + +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.5.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 new file mode 100644 index 0000000..2c2d9c7 --- /dev/null +++ b/tests/test_timeout_decorator.py @@ -0,0 +1,120 @@ +"""Timeout decorator tests.""" +import time + +import pytest + +from timeout_decorator import timeout, TimeoutError + + +@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) + with pytest.raises(TimeoutError): + f() + + +def test_timeout_class_method(use_signals): + class c(): + @timeout(1, use_signals=use_signals) + def f(self): + time.sleep(2) + with pytest.raises(TimeoutError): + c().f() + + +def test_timeout_kwargs(use_signals): + @timeout(3, use_signals=use_signals) + def f(): + time.sleep(2) + with pytest.raises(TimeoutError): + f(timeout=1) + + +def test_timeout_alternate_exception(use_signals): + @timeout(3, use_signals=use_signals, timeout_exception=StopIteration) + def f(): + time.sleep(2) + with pytest.raises(StopIteration): + f(timeout=1) + + +def test_timeout_kwargs_with_initial_timeout_none(use_signals): + @timeout(use_signals=use_signals) + def f(): + time.sleep(2) + with pytest.raises(TimeoutError): + f(timeout=1) + + +def test_timeout_no_seconds(use_signals): + @timeout(use_signals=use_signals) + def f(): + time.sleep(0.1) + f() + + +def test_timeout_partial_seconds(use_signals): + @timeout(0.2, use_signals=use_signals) + def f(): + time.sleep(0.5) + with pytest.raises(TimeoutError): + f() + + +def test_timeout_ok(use_signals): + @timeout(seconds=2, use_signals=use_signals) + def f(): + time.sleep(1) + f() + + +def test_function_name(use_signals): + @timeout(seconds=2, use_signals=use_signals) + def func_name(): + pass + + assert func_name.__name__ == 'func_name' + + +def test_timeout_pickle_error(): + """Test that when a pickle error occurs a timeout error is raised.""" + @timeout(seconds=1, use_signals=False) + def f(): + time.sleep(0.1) + + class Test(object): + pass + return Test() + with pytest.raises(TimeoutError): + f() + + +def test_timeout_custom_exception_message(): + @timeout(seconds=1, exception_message="Custom fail message") + def f(): + time.sleep(2) + with pytest.raises(TimeoutError, match="Custom fail message"): + f() + + +def test_timeout_custom_exception_with_message(): + @timeout(seconds=1, timeout_exception=RuntimeError, exception_message="Custom fail message") + def f(): + time.sleep(2) + with pytest.raises(RuntimeError, match="Custom fail message"): + f() + + +def test_timeout_default_exception_message(): + @timeout(seconds=1) + def f(): + time.sleep(2) + with pytest.raises(TimeoutError, match="Timed Out"): + f() diff --git a/timeout_decorator/__init__.py b/timeout_decorator/__init__.py new file mode 100644 index 0000000..4c8254b --- /dev/null +++ b/timeout_decorator/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from .timeout_decorator import timeout +from .timeout_decorator import TimeoutError + +__title__ = 'timeout_decorator' +__version__ = '0.5.0' diff --git a/timeout_decorator/timeout_decorator.py b/timeout_decorator/timeout_decorator.py new file mode 100644 index 0000000..42b6686 --- /dev/null +++ b/timeout_decorator/timeout_decorator.py @@ -0,0 +1,175 @@ +""" +Timeout decorator. + + :copyright: (c) 2012-2013 by PN. + :license: MIT, see LICENSE for more details. +""" + +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 + +############################################################ +# Timeout +############################################################ + +# 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 + + def __str__(self): + return repr(self.value) + + +def _raise_exception(exception, exception_message): + """ This function checks if a exception message is given. + + If there is no exception message, the default behaviour is maintained. + If there is an exception message, the message is passed to the exception with the 'value' keyword. + """ + if exception_message is None: + raise exception() + else: + raise exception(exception_message) + + +def timeout(seconds=None, use_signals=True, timeout_exception=TimeoutError, exception_message=None): + """Add a timeout parameter to a function and return it. + + :param seconds: optional time limit in seconds or fractions of a second. 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: float + :param use_signals: flag indicating whether signals should be used for timing function out or the multiprocessing + When using multiprocessing, timeout granularity is limited to 10ths of a second. + :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 use_signals: + def handler(signum, frame): + _raise_exception(timeout_exception, exception_message) + + @wraps(function) + def new_function(*args, **kwargs): + new_seconds = kwargs.pop('timeout', seconds) + if new_seconds: + old = signal.signal(signal.SIGALRM, handler) + signal.setitimer(signal.ITIMER_REAL, new_seconds) + + if not seconds: + return function(*args, **kwargs) + + try: + return function(*args, **kwargs) + finally: + if new_seconds: + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, old) + return new_function + else: + @wraps(function) + def new_function(*args, **kwargs): + timeout_wrapper = _Timeout(function, timeout_exception, exception_message, seconds) + return timeout_wrapper(*args, **kwargs) + return new_function + + 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(object): + + """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, timeout_exception, exception_message, limit): + """Initialize instance in preparation for being called.""" + self.__limit = limit + self.__function = function + self.__timeout_exception = timeout_exception + self.__exception_message = exception_message + 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.__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() + if self.__limit is not None: + 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_exception(self.__timeout_exception, self.__exception_message) + + @property + def ready(self): + """Read-only property indicating status of "value" property.""" + if self.__limit and self.__timeout < time.time(): + self.cancel() + return self.__queue.full() and not self.__queue.empty() + + @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,17 @@ +[tox] +distshare={homedir}/.tox/distshare +envlist=py{27,36,37,38} +skip_missing_interpreters=true +indexserver= + pypi = https://pypi.python.org/simple + +[testenv] +commands= + py.test timeout_decorator tests +deps = + pytest + pytest-pep8 + +[pytest] +addopts = -vvl +pep8maxlinelength=120 |