src/eric7/DebugClients/Python/coverage/pytracer.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8991
2fc945191992
child 9252
32dd11232e06
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3
4 """Raw data collector for coverage.py."""
5
6 import atexit
7 import dis
8 import sys
9
10 from coverage import env
11
12 # We need the YIELD_VALUE opcode below, in a comparison-friendly form.
13 RESUME = dis.opmap.get('RESUME')
14 RETURN_VALUE = dis.opmap['RETURN_VALUE']
15 if RESUME is None:
16 YIELD_VALUE = dis.opmap['YIELD_VALUE']
17 YIELD_FROM = dis.opmap['YIELD_FROM']
18 YIELD_FROM_OFFSET = 0 if env.PYPY else 2
19
20 # When running meta-coverage, this file can try to trace itself, which confuses
21 # everything. Don't trace ourselves.
22
23 THIS_FILE = __file__.rstrip("co")
24
25 class PyTracer:
26 """Python implementation of the raw data tracer."""
27
28 # Because of poor implementations of trace-function-manipulating tools,
29 # the Python trace function must be kept very simple. In particular, there
30 # must be only one function ever set as the trace function, both through
31 # sys.settrace, and as the return value from the trace function. Put
32 # another way, the trace function must always return itself. It cannot
33 # swap in other functions, or return None to avoid tracing a particular
34 # frame.
35 #
36 # The trace manipulator that introduced this restriction is DecoratorTools,
37 # which sets a trace function, and then later restores the pre-existing one
38 # by calling sys.settrace with a function it found in the current frame.
39 #
40 # Systems that use DecoratorTools (or similar trace manipulations) must use
41 # PyTracer to get accurate results. The command-line --timid argument is
42 # used to force the use of this tracer.
43
44 def __init__(self):
45 # Attributes set from the collector:
46 self.data = None
47 self.trace_arcs = False
48 self.should_trace = None
49 self.should_trace_cache = None
50 self.should_start_context = None
51 self.warn = None
52 # The threading module to use, if any.
53 self.threading = None
54
55 self.cur_file_data = None
56 self.last_line = 0 # int, but uninitialized.
57 self.cur_file_name = None
58 self.context = None
59 self.started_context = False
60
61 self.data_stack = []
62 self.thread = None
63 self.stopped = False
64 self._activity = False
65
66 self.in_atexit = False
67 # On exit, self.in_atexit = True
68 atexit.register(setattr, self, 'in_atexit', True)
69
70 def __repr__(self):
71 return "<PyTracer at 0x{:x}: {} lines in {} files>".format(
72 id(self),
73 sum(len(v) for v in self.data.values()),
74 len(self.data),
75 )
76
77 def log(self, marker, *args):
78 """For hard-core logging of what this tracer is doing."""
79 with open("/tmp/debug_trace.txt", "a") as f:
80 f.write("{} {}[{}]".format(
81 marker,
82 id(self),
83 len(self.data_stack),
84 ))
85 if 0: # if you want thread ids..
86 f.write(".{:x}.{:x}".format(
87 self.thread.ident,
88 self.threading.current_thread().ident,
89 ))
90 f.write(" {}".format(" ".join(map(str, args))))
91 if 0: # if you want callers..
92 f.write(" | ")
93 stack = " / ".join(
94 (fname or "???").rpartition("/")[-1]
95 for _, fname, _, _ in self.data_stack
96 )
97 f.write(stack)
98 f.write("\n")
99
100 def _trace(self, frame, event, arg_unused):
101 """The trace function passed to sys.settrace."""
102
103 if THIS_FILE in frame.f_code.co_filename:
104 return None
105
106 #self.log(":", frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + "()", event)
107
108 if (self.stopped and sys.gettrace() == self._trace): # pylint: disable=comparison-with-callable
109 # The PyTrace.stop() method has been called, possibly by another
110 # thread, let's deactivate ourselves now.
111 if 0:
112 self.log("---\nX", frame.f_code.co_filename, frame.f_lineno)
113 f = frame
114 while f:
115 self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace)
116 f = f.f_back
117 sys.settrace(None)
118 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = (
119 self.data_stack.pop()
120 )
121 return None
122
123 # if event != 'call' and frame.f_code.co_filename != self.cur_file_name:
124 # self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno)
125
126 if event == 'call':
127 # Should we start a new context?
128 if self.should_start_context and self.context is None:
129 context_maybe = self.should_start_context(frame)
130 if context_maybe is not None:
131 self.context = context_maybe
132 self.started_context = True
133 self.switch_context(self.context)
134 else:
135 self.started_context = False
136 else:
137 self.started_context = False
138
139 # Entering a new frame. Decide if we should trace in this file.
140 self._activity = True
141 self.data_stack.append(
142 (
143 self.cur_file_data,
144 self.cur_file_name,
145 self.last_line,
146 self.started_context,
147 )
148 )
149 filename = frame.f_code.co_filename
150 self.cur_file_name = filename
151 disp = self.should_trace_cache.get(filename)
152 if disp is None:
153 disp = self.should_trace(filename, frame)
154 self.should_trace_cache[filename] = disp
155
156 self.cur_file_data = None
157 if disp.trace:
158 tracename = disp.source_filename
159 if tracename not in self.data:
160 self.data[tracename] = set()
161 self.cur_file_data = self.data[tracename]
162 # The call event is really a "start frame" event, and happens for
163 # function calls and re-entering generators. The f_lasti field is
164 # -1 for calls, and a real offset for generators. Use <0 as the
165 # line number for calls, and the real line number for generators.
166 if RESUME is not None:
167 # The current opcode is guaranteed to be RESUME. The argument
168 # determines what kind of resume it is.
169 oparg = frame.f_code.co_code[frame.f_lasti + 1]
170 real_call = (oparg == 0)
171 else:
172 real_call = (getattr(frame, 'f_lasti', -1) < 0)
173 if real_call:
174 self.last_line = -frame.f_code.co_firstlineno
175 else:
176 self.last_line = frame.f_lineno
177 elif event == 'line':
178 # Record an executed line.
179 if self.cur_file_data is not None:
180 lineno = frame.f_lineno
181
182 if self.trace_arcs:
183 self.cur_file_data.add((self.last_line, lineno))
184 else:
185 self.cur_file_data.add(lineno)
186 self.last_line = lineno
187 elif event == 'return':
188 if self.trace_arcs and self.cur_file_data:
189 # Record an arc leaving the function, but beware that a
190 # "return" event might just mean yielding from a generator.
191 code = frame.f_code.co_code
192 lasti = frame.f_lasti
193 if RESUME is not None:
194 if len(code) == lasti + 2:
195 # A return from the end of a code object is a real return.
196 real_return = True
197 else:
198 # it's a real return.
199 real_return = (code[lasti + 2] != RESUME)
200 else:
201 if code[lasti] == RETURN_VALUE:
202 real_return = True
203 elif code[lasti] == YIELD_VALUE:
204 real_return = False
205 elif len(code) <= lasti + YIELD_FROM_OFFSET:
206 real_return = True
207 elif code[lasti + YIELD_FROM_OFFSET] == YIELD_FROM:
208 real_return = False
209 else:
210 real_return = True
211 if real_return:
212 first = frame.f_code.co_firstlineno
213 self.cur_file_data.add((self.last_line, -first))
214 # Leaving this function, pop the filename stack.
215 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = (
216 self.data_stack.pop()
217 )
218 # Leaving a context?
219 if self.started_context:
220 self.context = None
221 self.switch_context(None)
222 return self._trace
223
224 def start(self):
225 """Start this Tracer.
226
227 Return a Python function suitable for use with sys.settrace().
228
229 """
230 self.stopped = False
231 if self.threading:
232 if self.thread is None:
233 self.thread = self.threading.current_thread()
234 else:
235 if self.thread.ident != self.threading.current_thread().ident:
236 # Re-starting from a different thread!? Don't set the trace
237 # function, but we are marked as running again, so maybe it
238 # will be ok?
239 #self.log("~", "starting on different threads")
240 return self._trace
241
242 sys.settrace(self._trace)
243 return self._trace
244
245 def stop(self):
246 """Stop this Tracer."""
247 # Get the active tracer callback before setting the stop flag to be
248 # able to detect if the tracer was changed prior to stopping it.
249 tf = sys.gettrace()
250
251 # Set the stop flag. The actual call to sys.settrace(None) will happen
252 # in the self._trace callback itself to make sure to call it from the
253 # right thread.
254 self.stopped = True
255
256 if self.threading and self.thread.ident != self.threading.current_thread().ident:
257 # Called on a different thread than started us: we can't unhook
258 # ourselves, but we've set the flag that we should stop, so we
259 # won't do any more tracing.
260 #self.log("~", "stopping on different threads")
261 return
262
263 if self.warn:
264 # PyPy clears the trace function before running atexit functions,
265 # so don't warn if we are in atexit on PyPy and the trace function
266 # has changed to None.
267 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None)
268 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable
269 self.warn(
270 f"Trace function changed, data is likely wrong: {tf!r} != {self._trace!r}",
271 slug="trace-changed",
272 )
273
274 def activity(self):
275 """Has there been any activity?"""
276 return self._activity
277
278 def reset_activity(self):
279 """Reset the activity() flag."""
280 self._activity = False
281
282 def get_stats(self):
283 """Return a dictionary of statistics, or None."""
284 return None

eric ide

mercurial