eric7/DebugClients/Python/coverage/pytracer.py

branch
eric7
changeset 8312
800c432b34c8
parent 7975
7d493839a8fc
child 8527
2bd1325d727e
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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 YIELD_VALUE = dis.opmap['YIELD_VALUE']
14 if env.PY2:
15 YIELD_VALUE = chr(YIELD_VALUE)
16
17
18 class PyTracer(object):
19 """Python implementation of the raw data tracer."""
20
21 # Because of poor implementations of trace-function-manipulating tools,
22 # the Python trace function must be kept very simple. In particular, there
23 # must be only one function ever set as the trace function, both through
24 # sys.settrace, and as the return value from the trace function. Put
25 # another way, the trace function must always return itself. It cannot
26 # swap in other functions, or return None to avoid tracing a particular
27 # frame.
28 #
29 # The trace manipulator that introduced this restriction is DecoratorTools,
30 # which sets a trace function, and then later restores the pre-existing one
31 # by calling sys.settrace with a function it found in the current frame.
32 #
33 # Systems that use DecoratorTools (or similar trace manipulations) must use
34 # PyTracer to get accurate results. The command-line --timid argument is
35 # used to force the use of this tracer.
36
37 def __init__(self):
38 # Attributes set from the collector:
39 self.data = None
40 self.trace_arcs = False
41 self.should_trace = None
42 self.should_trace_cache = None
43 self.should_start_context = None
44 self.warn = None
45 # The threading module to use, if any.
46 self.threading = None
47
48 self.cur_file_dict = None
49 self.last_line = 0 # int, but uninitialized.
50 self.cur_file_name = None
51 self.context = None
52 self.started_context = False
53
54 self.data_stack = []
55 self.last_exc_back = None
56 self.last_exc_firstlineno = 0
57 self.thread = None
58 self.stopped = False
59 self._activity = False
60
61 self.in_atexit = False
62 # On exit, self.in_atexit = True
63 atexit.register(setattr, self, 'in_atexit', True)
64
65 def __repr__(self):
66 return "<PyTracer at {}: {} lines in {} files>".format(
67 id(self),
68 sum(len(v) for v in self.data.values()),
69 len(self.data),
70 )
71
72 def log(self, marker, *args):
73 """For hard-core logging of what this tracer is doing."""
74 with open("/tmp/debug_trace.txt", "a") as f:
75 f.write("{} {:x}.{:x}[{}] {:x} {}\n".format(
76 marker,
77 id(self),
78 self.thread.ident,
79 len(self.data_stack),
80 self.threading.currentThread().ident,
81 " ".join(map(str, args))
82 ))
83
84 def _trace(self, frame, event, arg_unused):
85 """The trace function passed to sys.settrace."""
86
87 #self.log(":", frame.f_code.co_filename, frame.f_lineno, event)
88
89 if (self.stopped and sys.gettrace() == self._trace): # pylint: disable=comparison-with-callable
90 # The PyTrace.stop() method has been called, possibly by another
91 # thread, let's deactivate ourselves now.
92 #self.log("X", frame.f_code.co_filename, frame.f_lineno)
93 sys.settrace(None)
94 return None
95
96 if self.last_exc_back:
97 if frame == self.last_exc_back:
98 # Someone forgot a return event.
99 if self.trace_arcs and self.cur_file_dict:
100 pair = (self.last_line, -self.last_exc_firstlineno)
101 self.cur_file_dict[pair] = None
102 self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = (
103 self.data_stack.pop()
104 )
105 self.last_exc_back = None
106
107 if event == 'call':
108 # Should we start a new context?
109 if self.should_start_context and self.context is None:
110 context_maybe = self.should_start_context(frame) # pylint: disable=not-callable
111 if context_maybe is not None:
112 self.context = context_maybe
113 self.started_context = True
114 self.switch_context(self.context)
115 else:
116 self.started_context = False
117 else:
118 self.started_context = False
119
120 # Entering a new frame. Decide if we should trace
121 # in this file.
122 self._activity = True
123 self.data_stack.append(
124 (
125 self.cur_file_dict,
126 self.cur_file_name,
127 self.last_line,
128 self.started_context,
129 )
130 )
131 filename = frame.f_code.co_filename
132 self.cur_file_name = filename
133 disp = self.should_trace_cache.get(filename)
134 if disp is None:
135 disp = self.should_trace(filename, frame) # pylint: disable=not-callable
136 self.should_trace_cache[filename] = disp # pylint: disable=unsupported-assignment-operation
137
138 self.cur_file_dict = None
139 if disp.trace:
140 tracename = disp.source_filename
141 if tracename not in self.data: # pylint: disable=unsupported-membership-test
142 self.data[tracename] = {} # pylint: disable=unsupported-assignment-operation
143 self.cur_file_dict = self.data[tracename] # pylint: disable=unsubscriptable-object
144 # The call event is really a "start frame" event, and happens for
145 # function calls and re-entering generators. The f_lasti field is
146 # -1 for calls, and a real offset for generators. Use <0 as the
147 # line number for calls, and the real line number for generators.
148 if getattr(frame, 'f_lasti', -1) < 0:
149 self.last_line = -frame.f_code.co_firstlineno
150 else:
151 self.last_line = frame.f_lineno
152 elif event == 'line':
153 # Record an executed line.
154 if self.cur_file_dict is not None:
155 lineno = frame.f_lineno
156 #if frame.f_code.co_filename != self.cur_file_name:
157 # self.log("*", frame.f_code.co_filename, self.cur_file_name, lineno)
158 if self.trace_arcs:
159 self.cur_file_dict[(self.last_line, lineno)] = None
160 else:
161 self.cur_file_dict[lineno] = None
162 self.last_line = lineno
163 elif event == 'return':
164 if self.trace_arcs and self.cur_file_dict:
165 # Record an arc leaving the function, but beware that a
166 # "return" event might just mean yielding from a generator.
167 # Jython seems to have an empty co_code, so just assume return.
168 code = frame.f_code.co_code
169 if (not code) or code[frame.f_lasti] != YIELD_VALUE:
170 first = frame.f_code.co_firstlineno
171 self.cur_file_dict[(self.last_line, -first)] = None
172 # Leaving this function, pop the filename stack.
173 self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = (
174 self.data_stack.pop()
175 )
176 # Leaving a context?
177 if self.started_context:
178 self.context = None
179 self.switch_context(None)
180 elif event == 'exception':
181 self.last_exc_back = frame.f_back
182 self.last_exc_firstlineno = frame.f_code.co_firstlineno
183 return self._trace
184
185 def start(self):
186 """Start this Tracer.
187
188 Return a Python function suitable for use with sys.settrace().
189
190 """
191 self.stopped = False
192 if self.threading:
193 if self.thread is None:
194 self.thread = self.threading.currentThread()
195 else:
196 if self.thread.ident != self.threading.currentThread().ident:
197 # Re-starting from a different thread!? Don't set the trace
198 # function, but we are marked as running again, so maybe it
199 # will be ok?
200 #self.log("~", "starting on different threads")
201 return self._trace
202
203 sys.settrace(self._trace)
204 return self._trace
205
206 def stop(self):
207 """Stop this Tracer."""
208 # Get the active tracer callback before setting the stop flag to be
209 # able to detect if the tracer was changed prior to stopping it.
210 tf = sys.gettrace()
211
212 # Set the stop flag. The actual call to sys.settrace(None) will happen
213 # in the self._trace callback itself to make sure to call it from the
214 # right thread.
215 self.stopped = True
216
217 if self.threading and self.thread.ident != self.threading.currentThread().ident:
218 # Called on a different thread than started us: we can't unhook
219 # ourselves, but we've set the flag that we should stop, so we
220 # won't do any more tracing.
221 #self.log("~", "stopping on different threads")
222 return
223
224 if self.warn:
225 # PyPy clears the trace function before running atexit functions,
226 # so don't warn if we are in atexit on PyPy and the trace function
227 # has changed to None.
228 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None)
229 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable
230 self.warn( # pylint: disable=not-callable
231 "Trace function changed, measurement is likely wrong: %r" % (tf,),
232 slug="trace-changed",
233 )
234
235 def activity(self):
236 """Has there been any activity?"""
237 return self._activity
238
239 def reset_activity(self):
240 """Reset the activity() flag."""
241 self._activity = False
242
243 def get_stats(self):
244 """Return a dictionary of statistics, or None."""
245 return None

eric ide

mercurial