recursion.py 4.83 KB
Newer Older
Stelios Karozis's avatar
Stelios Karozis committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
"""
Recursions are the recipe of |jedi| to conquer Python code. However, someone
must stop recursions going mad. Some settings are here to make |jedi| stop at
the right time. You can read more about them :ref:`here <settings-recursion>`.

Next to the internal ``jedi.inference.cache`` this module also makes |jedi| not
thread-safe, because ``execution_recursion_decorator`` uses class variables to
count the function calls.

.. _settings-recursion:

Settings
~~~~~~~~~~

Recursion settings are important if you don't want extremly
recursive python code to go absolutely crazy.

The default values are based on experiments while completing the |jedi| library
itself (inception!). But I don't think there's any other Python library that
uses recursion in a similarly extreme way. Completion should also be fast and
therefore the quality might not always be maximal.

.. autodata:: recursion_limit
.. autodata:: total_function_execution_limit
.. autodata:: per_function_execution_limit
.. autodata:: per_function_recursion_limit
"""

from contextlib import contextmanager

from jedi import debug
from jedi.inference.base_value import NO_VALUES


recursion_limit = 15
"""
Like :func:`sys.getrecursionlimit()`, just for |jedi|.
"""
total_function_execution_limit = 200
"""
This is a hard limit of how many non-builtin functions can be executed.
"""
per_function_execution_limit = 6
"""
The maximal amount of times a specific function may be executed.
"""
per_function_recursion_limit = 2
"""
A function may not be executed more than this number of times recursively.
"""


class RecursionDetector(object):
    def __init__(self):
        self.pushed_nodes = []


@contextmanager
def execution_allowed(inference_state, node):
    """
    A decorator to detect recursions in statements. In a recursion a statement
    at the same place, in the same module may not be executed two times.
    """
    pushed_nodes = inference_state.recursion_detector.pushed_nodes

    if node in pushed_nodes:
        debug.warning('catched stmt recursion: %s @%s', node,
                      getattr(node, 'start_pos', None))
        yield False
    else:
        try:
            pushed_nodes.append(node)
            yield True
        finally:
            pushed_nodes.pop()


def execution_recursion_decorator(default=NO_VALUES):
    def decorator(func):
        def wrapper(self, **kwargs):
            detector = self.inference_state.execution_recursion_detector
            limit_reached = detector.push_execution(self)
            try:
                if limit_reached:
                    result = default
                else:
                    result = func(self, **kwargs)
            finally:
                detector.pop_execution()
            return result
        return wrapper
    return decorator


class ExecutionRecursionDetector(object):
    """
    Catches recursions of executions.
    """
    def __init__(self, inference_state):
        self._inference_state = inference_state

        self._recursion_level = 0
        self._parent_execution_funcs = []
        self._funcdef_execution_counts = {}
        self._execution_count = 0

    def pop_execution(self):
        self._parent_execution_funcs.pop()
        self._recursion_level -= 1

    def push_execution(self, execution):
        funcdef = execution.tree_node

        # These two will be undone in pop_execution.
        self._recursion_level += 1
        self._parent_execution_funcs.append(funcdef)

        module_context = execution.get_root_context()

        if module_context.is_builtins_module():
            # We have control over builtins so we know they are not recursing
            # like crazy. Therefore we just let them execute always, because
            # they usually just help a lot with getting good results.
            return False

        if self._recursion_level > recursion_limit:
            debug.warning('Recursion limit (%s) reached', recursion_limit)
            return True

        if self._execution_count >= total_function_execution_limit:
            debug.warning('Function execution limit (%s) reached', total_function_execution_limit)
            return True
        self._execution_count += 1

        if self._funcdef_execution_counts.setdefault(funcdef, 0) >= per_function_execution_limit:
            if module_context.py__name__() == 'typing':
                return False
            debug.warning(
                'Per function execution limit (%s) reached: %s',
                per_function_execution_limit,
                funcdef
            )
            return True
        self._funcdef_execution_counts[funcdef] += 1

        if self._parent_execution_funcs.count(funcdef) > per_function_recursion_limit:
            debug.warning(
                'Per function recursion limit (%s) reached: %s',
                per_function_recursion_limit,
                funcdef
            )
            return True
        return False