func_timeout
func_timeout copied to clipboard
Cannot use it in a nested manner
I'm using it under Python 3.8.4
from time import sleep
from func_timeout import func_set_timeout
@func_set_timeout(1)
def func_1(sec):
print(f"in func 1, sleeping {sec} sec but will timeout in 1 sec")
sleep(sec)
@func_set_timeout(1)
def func_2(sec):
print(f"in func 2, sleeping {sec} sec but will timeout in 1 sec")
sleep(sec)
@func_set_timeout(5)
def func():
func_1(2)
print("in main func")
func_2(0.3)
if __name__ == "__main__":
func()
This program would stop at func_1(2) silently
it seems the top level @func_set_timeout(5) swallows all the FunctionTimedOut and doesn't re-raise
i think this is why but i don't understand the reason behind it https://github.com/kata198/func_timeout/blob/50baa8db502fd24acc0f2a1bc473649505336997/func_timeout/dafunc.py#L69-L71
in order to use it in a nested manner, we need to capture the FunctionTimedOut and re-raise as something else such as TimeoutError for it to be captured by the top-level @func_set_timeout(5)
from functools import wraps
from time import sleep
from typing import Any, Callable, Type
from func_timeout import FunctionTimedOut, func_set_timeout
def retry(
*exceptions: Type[Exception], count: int = 20, wait_time: int = 20,
) -> Callable:
"""Enable retry option for a set of exceptions
"""
def retry_decorator(func: Callable) -> Callable:
"""Decorator"""
@wraps(func)
def func_wrapper(
*args: Any, **kwargs: Any
) -> Any:
"""Function wrapper"""
for i in range(count):
try:
return func(*args, **kwargs)
except exceptions as exception: # pragma: no cover
print(
f"Retrying {i+1}: exception='{exception}', "
f"total {count} retries, wait {wait_time} seconds"
)
sleep(wait_time)
if i == count - 1:
if isinstance(exception, FunctionTimedOut):
raise TimeoutError("Re-raise as TimeoutError")
raise exception
return func_wrapper
return retry_decorator
@retry(
FunctionTimedOut, count=2, wait_time=1,
)
@func_set_timeout(1)
def func_1(sec):
print(f"in func 1, sleeping {sec} sec but will timeout in 1 sec")
sleep(sec)
@retry(
FunctionTimedOut, count=2, wait_time=1,
)
@func_set_timeout(1)
def func_2(sec):
print(f"in func 2, sleeping {sec} sec but will timeout in 1 sec")
sleep(sec)
@retry(
FunctionTimedOut, count=2, wait_time=1,
)
@func_set_timeout(5)
def func():
func_1(2)
print("in main func")
func_2(0.3)
if __name__ == "__main__":
func()
Sorry for the delay, I've been away.
I've fixed this issue. The change is committed under the currently unreleased 4.4branch (I need to write tests and such)
If you want technical details on why that "pass" statement is there, it's to avoid the default exception handler which we cannot control, and it spams stderr. You used to be able to override it, but not on anything modern. You can see the spam if you change that "pass" to "raise" and run the test suite.
I've updated the code so that we now explicitly check if we are going to raise the exception into another StoppableThread, in which case 4.4branch now does. Otherwise, (returning to the MainThread or a different type of thread) we continue to eat the error.
The test suite passes with this change, and your testcase also passes. I'll write some test cases to make sure this covers the issue completely, and it stays covered.
Thanks for the report!