Something like this:
def timeout(timeout, raise_exc=True):
"""
raise_exc - if exception should be raised on timeout
or exception inside decorated func.
Otherwise None will be returned.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
res = None
exc = None
def _run():
nonlocal res
nonlocal exc
try:
res = func(*args, **kwargs)
except Exception as e:
exc = e
t = threading.Thread(target=_run)
t.daemon = True
t.start()
t.join(timeout=timeout)
if raise_exc and t.is_alive():
raise TimeoutError()
elif raise_exc and (exc is not None):
raise exc
else:
return res
return wrapper
return decorator
Examples:
@timeout(0.5, raise_exc=False)
def outer():
return inner()
@timeout(2)
def inner():
time.sleep(1)
return "Shouldn't be printed"
print(outer()) # None
and
@timeout(2, raise_exc=False)
def outer():
return inner()
@timeout(2)
def inner():
time.sleep(1)
return "Should be printed"
print(outer()) # Should be printed
Note, that your task can be solved only with threads or processes, but this may lead to some non-obvious problems. I recommend you to think if your task can be solved without it. In most cases you can split your code to parts and check for timeout after each. Something like this:
def outer(arg, timeout=None):
t = Timeout(timeout)
# some operation:
time.sleep(1)
if t.is_timeout: return None
# use time left as subfunction's timeout:
return inner(arg, timeout=t.time_left)