Search code examples
pythonrecursionmetaprogrammingtrac

Detecting infinite recursion


I'm creating a macro for Trac, and one of the things it does is to render a bit of wiki text, that can in turn use the same macro.

This can give rise to an infinite recursion if the inner macro is invoked with the same arguments (i.e., renders the same bit of wiki text). I thought of trying to stop the user from shooting his own foot like this by inspecting the call stack and breaking the recursion if the function that expands the macro was already invoked with exactly the same set of arguments.

I've been looking at the inspect module, which definitely seems like the way to go, but still couldn't figure out how to discover the argument values of the previous function on the stack. How can I do this?


Solution

  • Catching the recursion exception is the better approach, but you could also add a decorator on the functions you wanted to 'protect':

    from functools import wraps
    from threading import local
    
    def recursion_detector(func):
        func._thread_locals = local()
    
        @wraps(func)
        def wrapper(*args, **kwargs):
            params = tuple(args) + tuple(kwargs.items())
    
            if not hasattr(func._thread_locals, 'seen'):
                func._thread_locals.seen = set()
            if params in func._thread_locals.seen:
                raise RuntimeError('Already called this function with the same arguments')
    
            func._thread_locals.seen.add(params)
            try:
                res = func(*args, **kwargs)
            finally:
                func._thread_locals.seen.remove(params)
    
            return res
    
        return wrapper
    

    then apply that decorator to the macro render function.

    A simple demo:

    >>> @recursion_detector
    ... def foo(bar):
    ...     return foo(not bar)
    ... 
    >>> foo(True)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 7, in wrapper
    RuntimeError: Already called this function with the same arguments
    >>> foo(False)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 7, in wrapper
    RuntimeError: Already called this function with the same arguments