]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """ |
2 | Recursions are the recipe of |jedi| to conquer Python code. However, someone | |
3 | must stop recursions going mad. Some settings are here to make |jedi| stop at | |
4 | the right time. You can read more about them :ref:`here <settings-recursion>`. | |
5 | ||
6 | Next to the internal ``jedi.inference.cache`` this module also makes |jedi| not | |
7 | thread-safe, because ``execution_recursion_decorator`` uses class variables to | |
8 | count the function calls. | |
9 | ||
10 | .. _settings-recursion: | |
11 | ||
12 | Settings | |
13 | ~~~~~~~~~~ | |
14 | ||
15 | Recursion settings are important if you don't want extremely | |
16 | recursive python code to go absolutely crazy. | |
17 | ||
18 | The default values are based on experiments while completing the |jedi| library | |
19 | itself (inception!). But I don't think there's any other Python library that | |
20 | uses recursion in a similarly extreme way. Completion should also be fast and | |
21 | therefore the quality might not always be maximal. | |
22 | ||
23 | .. autodata:: recursion_limit | |
24 | .. autodata:: total_function_execution_limit | |
25 | .. autodata:: per_function_execution_limit | |
26 | .. autodata:: per_function_recursion_limit | |
27 | """ | |
28 | ||
29 | from contextlib import contextmanager | |
30 | ||
31 | from jedi import debug | |
32 | from jedi.inference.base_value import NO_VALUES | |
33 | ||
34 | ||
35 | recursion_limit = 15 | |
36 | """ | |
37 | Like :func:`sys.getrecursionlimit()`, just for |jedi|. | |
38 | """ | |
39 | total_function_execution_limit = 200 | |
40 | """ | |
41 | This is a hard limit of how many non-builtin functions can be executed. | |
42 | """ | |
43 | per_function_execution_limit = 6 | |
44 | """ | |
45 | The maximal amount of times a specific function may be executed. | |
46 | """ | |
47 | per_function_recursion_limit = 2 | |
48 | """ | |
49 | A function may not be executed more than this number of times recursively. | |
50 | """ | |
51 | ||
52 | ||
53 | class RecursionDetector: | |
54 | def __init__(self): | |
55 | self.pushed_nodes = [] | |
56 | ||
57 | ||
58 | @contextmanager | |
59 | def execution_allowed(inference_state, node): | |
60 | """ | |
61 | A decorator to detect recursions in statements. In a recursion a statement | |
62 | at the same place, in the same module may not be executed two times. | |
63 | """ | |
64 | pushed_nodes = inference_state.recursion_detector.pushed_nodes | |
65 | ||
66 | if node in pushed_nodes: | |
67 | debug.warning('catched stmt recursion: %s @%s', node, | |
68 | getattr(node, 'start_pos', None)) | |
69 | yield False | |
70 | else: | |
71 | try: | |
72 | pushed_nodes.append(node) | |
73 | yield True | |
74 | finally: | |
75 | pushed_nodes.pop() | |
76 | ||
77 | ||
78 | def execution_recursion_decorator(default=NO_VALUES): | |
79 | def decorator(func): | |
80 | def wrapper(self, **kwargs): | |
81 | detector = self.inference_state.execution_recursion_detector | |
82 | limit_reached = detector.push_execution(self) | |
83 | try: | |
84 | if limit_reached: | |
85 | result = default | |
86 | else: | |
87 | result = func(self, **kwargs) | |
88 | finally: | |
89 | detector.pop_execution() | |
90 | return result | |
91 | return wrapper | |
92 | return decorator | |
93 | ||
94 | ||
95 | class ExecutionRecursionDetector: | |
96 | """ | |
97 | Catches recursions of executions. | |
98 | """ | |
99 | def __init__(self, inference_state): | |
100 | self._inference_state = inference_state | |
101 | ||
102 | self._recursion_level = 0 | |
103 | self._parent_execution_funcs = [] | |
104 | self._funcdef_execution_counts = {} | |
105 | self._execution_count = 0 | |
106 | ||
107 | def pop_execution(self): | |
108 | self._parent_execution_funcs.pop() | |
109 | self._recursion_level -= 1 | |
110 | ||
111 | def push_execution(self, execution): | |
112 | funcdef = execution.tree_node | |
113 | ||
114 | # These two will be undone in pop_execution. | |
115 | self._recursion_level += 1 | |
116 | self._parent_execution_funcs.append(funcdef) | |
117 | ||
118 | module_context = execution.get_root_context() | |
119 | ||
120 | if module_context.is_builtins_module(): | |
121 | # We have control over builtins so we know they are not recursing | |
122 | # like crazy. Therefore we just let them execute always, because | |
123 | # they usually just help a lot with getting good results. | |
124 | return False | |
125 | ||
126 | if self._recursion_level > recursion_limit: | |
127 | debug.warning('Recursion limit (%s) reached', recursion_limit) | |
128 | return True | |
129 | ||
130 | if self._execution_count >= total_function_execution_limit: | |
131 | debug.warning('Function execution limit (%s) reached', total_function_execution_limit) | |
132 | return True | |
133 | self._execution_count += 1 | |
134 | ||
135 | if self._funcdef_execution_counts.setdefault(funcdef, 0) >= per_function_execution_limit: | |
136 | if module_context.py__name__() == 'typing': | |
137 | return False | |
138 | debug.warning( | |
139 | 'Per function execution limit (%s) reached: %s', | |
140 | per_function_execution_limit, | |
141 | funcdef | |
142 | ) | |
143 | return True | |
144 | self._funcdef_execution_counts[funcdef] += 1 | |
145 | ||
146 | if self._parent_execution_funcs.count(funcdef) > per_function_recursion_limit: | |
147 | debug.warning( | |
148 | 'Per function recursion limit (%s) reached: %s', | |
149 | per_function_recursion_limit, | |
150 | funcdef | |
151 | ) | |
152 | return True | |
153 | return False |