Merge lp:~pconnell/entrails/sextant into lp:entrails

Proposed by Phil Connell
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
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-an-obj-sort-of-a-dumb-container Frame (that wasn't a frame).

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-an-obj-sort-of-a-dumb-container Frame (that wasn't a frame).

To post a comment you must log in.
Revision history for this message
Martin Morrison (isoschiz) 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.

review: Needs Fixing
Revision history for this message
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...

Revision history for this message
ChrisD (gingerchris) wrote :

Ideal

Revision history for this message
ChrisD (gingerchris) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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()

Subscribers

People subscribed via source and target branches