Merge lp:~pconnell/entrails/sextant into lp:entrails
- sextant
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | ChrisD |
Approved revision: | 40 |
Merged at revision: | 25 |
Proposed branch: | lp:~pconnell/entrails/sextant |
Merge into: | lp:entrails |
Diff against target: |
497 lines (+199/-89) 4 files modified
src/entrails/_filter.py (+5/-6) src/entrails/_output.py (+51/-24) src/entrails/_trace.py (+140/-56) src/trace.py (+3/-3) |
To merge this branch: | bzr merge lp:~pconnell/entrails/sextant |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
ChrisD | Approve | ||
Martin Morrison | Pending | ||
Review via email: mp+233257@code.launchpad.net |
This proposal supersedes a proposal from 2014-08-15.
Commit message
Various changes made while getting entrails working with sextant.
Main externally-visible change is that entrails makes a slightly better effort to find the names of classes corresponding to method calls (i.e. get qualnames rather than just function names).
Some bits of internal cleanup too: have a properly structured 'Event' object rather than the weird sort-of-
Description of the change
Various changes made while getting entrails working with sextant.
Main externally-visible change is that entrails makes a slightly better effort to find the names of classes corresponding to method calls (i.e. get qualnames rather than just function names).
Some bits of internal cleanup too: have a properly structured 'Event' object rather than the weird sort-of-
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal | # |
Phil Connell (pconnell) wrote : Posted in a previous version of this proposal | # |
> EnTrails could really do with some UT to make me more confident that you
> hadn't missed any of the name changes... There are at least a couple inline
> that look wrong, but I've not read every line carefully to be sure there
> aren't others.
Completely agree on the UT front, but I'm not especially concerned about this right now:
- I'm pretty sure nobody uses any of this stuff.
- Outputting to sextant is likely to supercede the existing output formats for all practical purposes, and I think the most risk is to the outputs.
As it happens, after taking another look I'm pretty convinced that the HTMLOutput stuff was already not working...
ChrisD (gingerchris) wrote : | # |
Ideal
ChrisD (gingerchris) : | # |
Preview Diff
1 | === modified file 'src/entrails/_filter.py' |
2 | --- src/entrails/_filter.py 2014-07-10 18:08:27 +0000 |
3 | +++ src/entrails/_filter.py 2014-09-03 20:05:48 +0000 |
4 | @@ -36,9 +36,9 @@ |
5 | """ |
6 | matches = ("/usr/lib", "/usr/local/lib", "/usr/lib64", "/twisted", "/zope") |
7 | |
8 | - def filter(self, frameobject): |
9 | + def filter(self, event): |
10 | for m in self.matches: |
11 | - if m in frameobject['co_filename']: |
12 | + if m in event.code.co_filename: |
13 | return False |
14 | return True |
15 | |
16 | @@ -54,8 +54,7 @@ |
17 | "_utils", |
18 | )) |
19 | |
20 | - def filter(self, frameobject): |
21 | - filename = frameobject["co_filename"] |
22 | - return not any(filename.endswith(module) |
23 | - for module in self.modules) |
24 | + def filter(self, event): |
25 | + filename = event.code.co_filename |
26 | + return not any(filename.endswith(module) for module in self.modules) |
27 | |
28 | |
29 | === modified file 'src/entrails/_output.py' |
30 | --- src/entrails/_output.py 2014-07-07 12:48:49 +0000 |
31 | +++ src/entrails/_output.py 2014-09-03 20:05:48 +0000 |
32 | @@ -36,14 +36,25 @@ |
33 | def __init__(self): |
34 | pass |
35 | def close(self): |
36 | - pass |
37 | - def enter(self, o): |
38 | - pass |
39 | - def exit(self, o): |
40 | - pass |
41 | - def exception(self, o): |
42 | + """Invoked when no further tracing data is to be output.""" |
43 | + pass |
44 | + def enter(self, event): |
45 | + """Invoked with an entrails event, when a function is called.""" |
46 | + pass |
47 | + def exit(self, event): |
48 | + """Invoked with an entrails event, when a function returns.""" |
49 | + pass |
50 | + def exception(self, event): |
51 | + """ |
52 | + Invoked with an entrails event, when an exception is raised. |
53 | + |
54 | + As an exception propagates up the call stack, this method is called for |
55 | + every frame in which the exception is raised. |
56 | + |
57 | + """ |
58 | pass |
59 | def logging(self, o, event_type_string): |
60 | + #@@@ not used? |
61 | pass |
62 | |
63 | |
64 | @@ -76,15 +87,15 @@ |
65 | return "Call " + o.short_call_string() |
66 | |
67 | def _default_exit_func(self, o): |
68 | - return "Return from {0} with value {1}".format(o["co_name"], |
69 | + return "Return from {0} with value {1}".format(o.code.co_name, |
70 | o.short_return_value()) |
71 | |
72 | def _default_exception_func(self, o): |
73 | return "Encountered exception" #@@@ more info TODO |
74 | |
75 | def _default_prefix_func(self, o): |
76 | - return ("[%s][%03d:%s]" + " "*self.depth) % (o["label"], |
77 | - o["f_lineno"], |
78 | + return ("[%s][%03d:%s]" + " "*self.depth) % (o.label, |
79 | + o.frame.f_lineno, |
80 | _utils.pretty_resize_string(o.nice_filename(), 15, exact=True)) |
81 | |
82 | def enter(self, o): |
83 | @@ -103,7 +114,7 @@ |
84 | |
85 | def logging(self, o, event_type_string): |
86 | self.safe_write("ERROR TRACING with event: %s on line %d in %s\n" % ( |
87 | - event_type_string, o["f_lineno"], o["co_filename"])) |
88 | + event_type_string, o.frame.f_lineno, o.code.co_filename)) |
89 | return None |
90 | |
91 | |
92 | @@ -160,17 +171,17 @@ |
93 | return None |
94 | |
95 | def exception(self, o): |
96 | - print("EXCEPTION on line {0} in {1}".format(o["f_lineno"], |
97 | - o["co_filename"])) |
98 | + print("EXCEPTION on line {0} in {1}".format(o.frame.f_lineno, |
99 | + o.code.co_filename)) |
100 | for k, v in o.items(): |
101 | print(k + ": " + str(v)) |
102 | return None |
103 | |
104 | def logging(self, o, event_type_string): |
105 | - print("ERROR TRACING with event: {0} on line {1} in {2}".format(event_type_string, |
106 | - o["f_lineno"], |
107 | - o["co_filename"])) |
108 | - return None |
109 | + print("ERROR TRACING with event: {0} on line {1} in {2}".format( |
110 | + event_type_string, |
111 | + o.frame.f_lineno, |
112 | + o.code.co_filename)) |
113 | |
114 | |
115 | class HTMLOutput(EntrailsOutput): |
116 | @@ -186,14 +197,18 @@ |
117 | self.f.write("\n</body></html>\n") |
118 | self.f.close() |
119 | |
120 | - def enter(self, codeframe): |
121 | + def enter(self, event): |
122 | self.depth += 1 |
123 | - self.f.write(" > "*self.depth + "<b>Now entering:</b> " + codeframe["name"] + "<br />\n") |
124 | + self.f.write(" > " * self.depth + |
125 | + "<b>Now entering:</b> " + |
126 | + event.code.co_name + "<br />\n") |
127 | return None |
128 | |
129 | - def exit(self, codeframe): |
130 | + def exit(self, event): |
131 | self.depth -= 1 |
132 | - self.f.write(" > "*(self.depth+1) + "<b>and exiting:</b> " + codeframe["name"] + " with return value " + str(codeframe["return_value"]) + "<br />\n") |
133 | + self.f.write(" > " * (self.depth + 1) + "<b>and exiting:</b> " + |
134 | + event.code.co_name + " with return value " + |
135 | + str(event.return_value) + "<br />\n") |
136 | return None |
137 | |
138 | def exception(self, o): |
139 | @@ -216,10 +231,22 @@ |
140 | |
141 | def write_out(self, codeframe, event_type): |
142 | event = ET.SubElement(self.root, "event") |
143 | - event.attrib = {"timestamp" : "{0:f}".format(codeframe["timestamp"])} |
144 | - for k, v in codeframe.items(): |
145 | - if k != "timestamp": |
146 | - ET.SubElement(event, k).text = str(v) |
147 | + event.attrib = {"timestamp" : "{0:f}".format(codeframe.timestamp)} |
148 | + info = { |
149 | + "label": codeframe.label, |
150 | + "f_lineno": codeframe.frame.f_lineno, |
151 | + "f_back": codeframe.frame.f_back, |
152 | + "f_locals": codeframe.frame.f_locals, |
153 | + "co_name": codeframe.code.co_name, |
154 | + "co_argcount": codeframe.code.co_argcount, |
155 | + "co_filename": codeframe.code.co_filename, |
156 | + "co_names": codeframe.code.co_names, |
157 | + "co_varnames": codeframe.code.co_varnames, |
158 | + } |
159 | + if codeframe.return_value is not None: |
160 | + info["return_value"] = codeframe.return_value |
161 | + for k, v in info.items(): |
162 | + ET.SubElement(event, k).text = str(v) |
163 | ET.SubElement(event, "type").text = event_type |
164 | ET.SubElement(event, "call_string").text = codeframe.call_string() |
165 | ET.SubElement(event, "short_call_string").text = codeframe.short_call_string() |
166 | |
167 | === modified file 'src/entrails/_trace.py' |
168 | --- src/entrails/_trace.py 2014-07-07 12:48:49 +0000 |
169 | +++ src/entrails/_trace.py 2014-09-03 20:05:48 +0000 |
170 | @@ -12,6 +12,7 @@ |
171 | |
172 | __all__ = ( |
173 | "Entrails", |
174 | + "Event", |
175 | ) |
176 | |
177 | |
178 | @@ -19,29 +20,85 @@ |
179 | import os.path |
180 | import sys |
181 | import time |
182 | +import types |
183 | |
184 | from . import _filter |
185 | from . import _utils |
186 | |
187 | |
188 | -class _Frame(dict): |
189 | - """ |
190 | - Holds information about a frame. |
191 | - |
192 | - Use as a dict to access the important bits of the frame and arg |
193 | - parameters, along with timestamp and label. Also has convienience |
194 | - methods to generate function call string and class info. This is |
195 | - supplied to an output adapter's call(), return() and exception() |
196 | - methods. |
197 | - """ |
198 | +class Event(object): |
199 | + """ |
200 | + Holds information about a traced event. |
201 | + |
202 | + Attributes: |
203 | + arg: Additional argument passed to the sys.settrace-installed function. |
204 | + code: Code object being executed. |
205 | + frame: Frame object for the executing code. |
206 | + label: String passed when event was created. |
207 | + return_value: For function exit events the value returned, otherwise |
208 | + None. |
209 | + timestamp: Time, in seconds relative to initialisation, at which the |
210 | + event occurred. |
211 | + |
212 | + Instances are supplied to an output adapter's call(), return() and |
213 | + exception() methods. |
214 | + |
215 | + """ |
216 | + |
217 | + def __init__(self, frame, event, arg, |
218 | + label, timestamp): |
219 | + """ |
220 | + Initialise this event. |
221 | + |
222 | + frame: |
223 | + Frame object passed to trace function. |
224 | + event: |
225 | + Event string passed to trace function. |
226 | + arg: |
227 | + Final argument passed to trace function. |
228 | + label: |
229 | + String identifying the context for the event. |
230 | + timestamp: |
231 | + Time, in seconds relative to initialisation, at which the event |
232 | + occurred. |
233 | + |
234 | + """ |
235 | + self.arg = arg |
236 | + self.code = frame.f_code |
237 | + self.frame = frame |
238 | + self.label = label |
239 | + self.timestamp = timestamp |
240 | + |
241 | + self.return_value = (arg if event == "return" else None) |
242 | + |
243 | + def basename(self): |
244 | + """Returns the name of the function for this event's frame.""" |
245 | + cls, method = self._retrieve_cls_method() |
246 | + if method is not None: |
247 | + try: |
248 | + name = method.__qualname__ |
249 | + except AttributeError: |
250 | + # Old version, < 3.3. |
251 | + name = "{}.{}".format(cls.__name__, method.__name__) |
252 | + else: |
253 | + name = self.frame.f_code.co_name |
254 | + |
255 | + return name |
256 | + |
257 | + def qualname(self): |
258 | + """Returns the fully-qualified function name for this event's frame.""" |
259 | + return "{}.{}".format(self.frame.f_globals["__name__"], |
260 | + self.basename()) |
261 | + |
262 | def parameters(self): |
263 | """ |
264 | Returns dict of form {parameter_name: argument_value}. |
265 | """ |
266 | # We rely on CPython putting arguments at start of f_locals (in reverse |
267 | # order for some reason...). This is undocumented, but consistent. |
268 | - parameters = [(k, v) for k, v in self["f_locals"].items() |
269 | - if k in self["co_varnames"][0:self["co_argcount"]]] |
270 | + parameters = [(k, v) |
271 | + for k, v in self.frame.f_locals.items() |
272 | + if k in self.code.co_varnames[0:self.code.co_argcount]] |
273 | parameters.reverse() |
274 | return collections.OrderedDict(parameters) |
275 | |
276 | @@ -79,14 +136,13 @@ |
277 | |
278 | Only designed for 'call' event (i.e. function entry) |
279 | """ |
280 | + output = '{}({})'.format(self.qualname(), |
281 | + ', '.join(self.string_arguments())) |
282 | if self.is_method(): |
283 | - return '{0}.{1}({2})'.format(self.instance_name(), self['co_name'], |
284 | - ', '.join(self.string_arguments())) |
285 | - else: |
286 | - return '{0}({1})'.format(self['co_name'], |
287 | - ', '.join(self.string_arguments())) |
288 | + output += ' on instance {}'.format(self.instance_name()) |
289 | + return output |
290 | |
291 | - def short_call_string(self, length=40): |
292 | + def short_call_string(self, length=80): |
293 | """ |
294 | Returns a pretty string of the function call, shortened to at |
295 | most length characters. |
296 | @@ -95,7 +151,7 @@ |
297 | """ |
298 | return _utils.pretty_resize_string(self.call_string(), length) |
299 | |
300 | - def short_return_value(self, length=40): |
301 | + def short_return_value(self, length=80): |
302 | """ |
303 | Returns a pretty string representation of the return value, |
304 | shortened to at most length characters. |
305 | @@ -103,7 +159,7 @@ |
306 | Only designed for 'call' event (i.e. function entry) |
307 | """ |
308 | try: |
309 | - return _utils.pretty_resize_string(repr(self['return_value']), length) |
310 | + return _utils.pretty_resize_string(repr(self.return_value), length) |
311 | except: |
312 | return _utils.pretty_resize_string('<repr error>', length) |
313 | |
314 | @@ -120,7 +176,7 @@ |
315 | """ |
316 | Nice filename, without directory segment. |
317 | """ |
318 | - return os.path.basename(self["co_filename"]) |
319 | + return os.path.basename(self.code.co_filename) |
320 | |
321 | def is_method(self): |
322 | """ |
323 | @@ -131,7 +187,7 @@ |
324 | |
325 | TODO: What happens for class methods/other decorated methods? |
326 | """ |
327 | - if self['co_varnames'] and self['co_varnames'][0] == 'self': |
328 | + if self.code.co_varnames and self.code.co_varnames[0] == 'self': |
329 | return True |
330 | return False |
331 | |
332 | @@ -141,9 +197,12 @@ |
333 | Only designed for 'call' event (i.e. function entry) |
334 | We cache value after first proper lookup. |
335 | """ |
336 | - if 'instance_name' not in self: |
337 | - self['instance_name'] = self._retrieve_instance_name() |
338 | - return self['instance_name'] |
339 | + try: |
340 | + name = self._instance_name |
341 | + except AttributeError: |
342 | + name = self._retrieve_instance_name() |
343 | + self._instance_name = name |
344 | + return name |
345 | |
346 | def _retrieve_instance_name(self): |
347 | """ |
348 | @@ -153,22 +212,59 @@ |
349 | try: |
350 | # This is confusing. We go into the previous (parent) frame and |
351 | # find the local variable name in that pointing to a our class. |
352 | - for k, v in list(self['f_back'].f_locals.items()): |
353 | - if v == self['f_locals']['self']: |
354 | + for k, v in list(self.frame.f_back.f_locals.items()): |
355 | + if v == self.frame.f_locals['self']: |
356 | return k |
357 | except: |
358 | return '<lookup error>' |
359 | |
360 | - def class_name(self): |
361 | - """ |
362 | - Class name of method, or empty string if not method. |
363 | - Only designed for 'call' event (i.e. function entry) |
364 | - Should only be called on methods. |
365 | - """ |
366 | - try: |
367 | - return self["f_locals"]["self"].__class__.__name__ |
368 | - except: |
369 | - return '<lookup error>' |
370 | + def method(self): |
371 | + """ |
372 | + Attempt to find the method corresponding to this event, if any. |
373 | + |
374 | + On success, returns the function found. If the frame doesn't look like |
375 | + it corresponds to a method, `None` is returned. |
376 | + |
377 | + """ |
378 | + return self._retrieve_cls_method()[1] |
379 | + |
380 | + def _retrieve_cls_method(self): |
381 | + """ |
382 | + Attempt to find the method and class corresponding to this event. |
383 | + |
384 | + On success returns a class, method tuple, where the method is an |
385 | + attribute on the class returned (helpful for <3.3 where __qualname__ |
386 | + isn't available). |
387 | + |
388 | + If the event doesn't look like it corresponds to a method, returns |
389 | + None, None. |
390 | + |
391 | + """ |
392 | + if not self.code.co_varnames or self.code.co_varnames[0] != "self": |
393 | + return None, None |
394 | + |
395 | + meth_self = self.frame.f_locals["self"] |
396 | + meth_name = self.code.co_name |
397 | + is_unbound_method = False |
398 | + |
399 | + # Alternative approaches: |
400 | + # - Source code parsing via inspect. |
401 | + # - gc.get_referrers |
402 | + # - ? |
403 | + # |
404 | + # GC is plausible (but won't scale very well with number of objects). |
405 | + |
406 | + for cls in type(meth_self).mro(): |
407 | + try: |
408 | + candidate = getattr(cls, meth_name) |
409 | + except AttributeError: |
410 | + continue |
411 | + |
412 | + if callable(candidate): |
413 | + return cls, candidate |
414 | + |
415 | + else: |
416 | + return None, None |
417 | |
418 | |
419 | class Entrails(object): |
420 | @@ -184,7 +280,9 @@ |
421 | |
422 | def __init__(self, |
423 | label='<untitled>', |
424 | - filters=(_filter.LibraryFilter(), _filter.SelfFilter())): |
425 | + filters=None): |
426 | + if filters is None: |
427 | + filters = (_filter.LibraryFilter(), _filter.SelfFilter()) |
428 | self._outputs = [] |
429 | self._init_time = time.time() |
430 | self._label = label |
431 | @@ -198,19 +296,10 @@ |
432 | return True |
433 | return False |
434 | |
435 | - def _trace_function(self, trace, event, arg): |
436 | - # Build Frame |
437 | - obj = _Frame() |
438 | - obj["label"] = self._label |
439 | - obj["timestamp"] = time.time() - self._init_time |
440 | - obj["f_lineno"] = trace.f_lineno |
441 | - obj["f_back"] = trace.f_back |
442 | - obj["f_locals"] = trace.f_locals |
443 | - obj["co_name"] = trace.f_code.co_name |
444 | - obj["co_argcount"] = trace.f_code.co_argcount |
445 | - obj["co_filename"] = trace.f_code.co_filename |
446 | - obj["co_names"] = trace.f_code.co_names |
447 | - obj["co_varnames"] = trace.f_code.co_varnames |
448 | + def _trace_function(self, frame, event, arg): |
449 | + obj = Event(frame, event, arg, |
450 | + self._label, |
451 | + time.time() - self._init_time) |
452 | |
453 | # Filter |
454 | # If this object is to be discarded, return trace_function to |
455 | @@ -218,11 +307,6 @@ |
456 | if not self.filter(obj): |
457 | return self._trace_function |
458 | |
459 | - # Possibly add return_value. If event is non-return, arg isn't really |
460 | - # that useful. |
461 | - if event is 'return': |
462 | - obj["return_value"] = arg |
463 | - |
464 | if event in self.event_names: |
465 | for output in self._outputs: |
466 | #try: |
467 | |
468 | === modified file 'src/trace.py' |
469 | --- src/trace.py 2014-07-07 12:48:49 +0000 |
470 | +++ src/trace.py 2014-09-03 20:05:48 +0000 |
471 | @@ -47,7 +47,7 @@ |
472 | first=True |
473 | for line in fileinput.input(filename, inplace=1, backup=".backup"): |
474 | if first and line.startswith("#!/"): |
475 | - print(line, end=' ') |
476 | + print(line, end='') |
477 | continue |
478 | if first: |
479 | print(MARKER) |
480 | @@ -61,7 +61,7 @@ |
481 | print("_entrails_t.start_trace()") |
482 | print(ENDMARKER) |
483 | first = False |
484 | - print(line, end=' ') |
485 | + print(line, end='') |
486 | fileinput.close() |
487 | |
488 | |
489 | @@ -131,7 +131,7 @@ |
490 | if line.startswith(MARKER): |
491 | extra = True |
492 | if not extra: |
493 | - print(line, end=' ') |
494 | + print(line, end='') |
495 | if line.startswith(ENDMARKER): |
496 | extra = False |
497 | fileinput.close() |
EnTrails could really do with some UT to make me more confident that you hadn't missed any of the name changes... There are at least a couple inline that look wrong, but I've not read every line carefully to be sure there aren't others.