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
=== modified file 'src/entrails/_filter.py'
--- src/entrails/_filter.py 2014-07-10 18:08:27 +0000
+++ src/entrails/_filter.py 2014-09-03 20:05:48 +0000
@@ -36,9 +36,9 @@
36 """36 """
37 matches = ("/usr/lib", "/usr/local/lib", "/usr/lib64", "/twisted", "/zope")37 matches = ("/usr/lib", "/usr/local/lib", "/usr/lib64", "/twisted", "/zope")
3838
39 def filter(self, frameobject):39 def filter(self, event):
40 for m in self.matches:40 for m in self.matches:
41 if m in frameobject['co_filename']:41 if m in event.code.co_filename:
42 return False42 return False
43 return True43 return True
4444
@@ -54,8 +54,7 @@
54 "_utils",54 "_utils",
55 ))55 ))
5656
57 def filter(self, frameobject):57 def filter(self, event):
58 filename = frameobject["co_filename"]58 filename = event.code.co_filename
59 return not any(filename.endswith(module)59 return not any(filename.endswith(module) for module in self.modules)
60 for module in self.modules)
6160
6261
=== modified file 'src/entrails/_output.py'
--- src/entrails/_output.py 2014-07-07 12:48:49 +0000
+++ src/entrails/_output.py 2014-09-03 20:05:48 +0000
@@ -36,14 +36,25 @@
36 def __init__(self):36 def __init__(self):
37 pass37 pass
38 def close(self):38 def close(self):
39 pass39 """Invoked when no further tracing data is to be output."""
40 def enter(self, o):40 pass
41 pass41 def enter(self, event):
42 def exit(self, o):42 """Invoked with an entrails event, when a function is called."""
43 pass43 pass
44 def exception(self, o):44 def exit(self, event):
45 """Invoked with an entrails event, when a function returns."""
46 pass
47 def exception(self, event):
48 """
49 Invoked with an entrails event, when an exception is raised.
50
51 As an exception propagates up the call stack, this method is called for
52 every frame in which the exception is raised.
53
54 """
45 pass55 pass
46 def logging(self, o, event_type_string):56 def logging(self, o, event_type_string):
57 #@@@ not used?
47 pass58 pass
4859
4960
@@ -76,15 +87,15 @@
76 return "Call " + o.short_call_string()87 return "Call " + o.short_call_string()
7788
78 def _default_exit_func(self, o):89 def _default_exit_func(self, o):
79 return "Return from {0} with value {1}".format(o["co_name"],90 return "Return from {0} with value {1}".format(o.code.co_name,
80 o.short_return_value())91 o.short_return_value())
8192
82 def _default_exception_func(self, o):93 def _default_exception_func(self, o):
83 return "Encountered exception" #@@@ more info TODO94 return "Encountered exception" #@@@ more info TODO
8495
85 def _default_prefix_func(self, o):96 def _default_prefix_func(self, o):
86 return ("[%s][%03d:%s]" + " "*self.depth) % (o["label"],97 return ("[%s][%03d:%s]" + " "*self.depth) % (o.label,
87 o["f_lineno"],98 o.frame.f_lineno,
88 _utils.pretty_resize_string(o.nice_filename(), 15, exact=True))99 _utils.pretty_resize_string(o.nice_filename(), 15, exact=True))
89100
90 def enter(self, o):101 def enter(self, o):
@@ -103,7 +114,7 @@
103114
104 def logging(self, o, event_type_string):115 def logging(self, o, event_type_string):
105 self.safe_write("ERROR TRACING with event: %s on line %d in %s\n" % (116 self.safe_write("ERROR TRACING with event: %s on line %d in %s\n" % (
106 event_type_string, o["f_lineno"], o["co_filename"]))117 event_type_string, o.frame.f_lineno, o.code.co_filename))
107 return None118 return None
108119
109120
@@ -160,17 +171,17 @@
160 return None171 return None
161172
162 def exception(self, o):173 def exception(self, o):
163 print("EXCEPTION on line {0} in {1}".format(o["f_lineno"],174 print("EXCEPTION on line {0} in {1}".format(o.frame.f_lineno,
164 o["co_filename"]))175 o.code.co_filename))
165 for k, v in o.items():176 for k, v in o.items():
166 print(k + ": " + str(v))177 print(k + ": " + str(v))
167 return None178 return None
168179
169 def logging(self, o, event_type_string):180 def logging(self, o, event_type_string):
170 print("ERROR TRACING with event: {0} on line {1} in {2}".format(event_type_string,181 print("ERROR TRACING with event: {0} on line {1} in {2}".format(
171 o["f_lineno"],182 event_type_string,
172 o["co_filename"]))183 o.frame.f_lineno,
173 return None184 o.code.co_filename))
174185
175186
176class HTMLOutput(EntrailsOutput):187class HTMLOutput(EntrailsOutput):
@@ -186,14 +197,18 @@
186 self.f.write("\n</body></html>\n")197 self.f.write("\n</body></html>\n")
187 self.f.close()198 self.f.close()
188199
189 def enter(self, codeframe):200 def enter(self, event):
190 self.depth += 1201 self.depth += 1
191 self.f.write(" > "*self.depth + "<b>Now entering:</b> " + codeframe["name"] + "<br />\n")202 self.f.write(" > " * self.depth +
203 "<b>Now entering:</b> " +
204 event.code.co_name + "<br />\n")
192 return None205 return None
193206
194 def exit(self, codeframe):207 def exit(self, event):
195 self.depth -= 1208 self.depth -= 1
196 self.f.write(" > "*(self.depth+1) + "<b>and exiting:</b> " + codeframe["name"] + " with return value " + str(codeframe["return_value"]) + "<br />\n")209 self.f.write(" > " * (self.depth + 1) + "<b>and exiting:</b> " +
210 event.code.co_name + " with return value " +
211 str(event.return_value) + "<br />\n")
197 return None212 return None
198213
199 def exception(self, o):214 def exception(self, o):
@@ -216,10 +231,22 @@
216231
217 def write_out(self, codeframe, event_type):232 def write_out(self, codeframe, event_type):
218 event = ET.SubElement(self.root, "event")233 event = ET.SubElement(self.root, "event")
219 event.attrib = {"timestamp" : "{0:f}".format(codeframe["timestamp"])}234 event.attrib = {"timestamp" : "{0:f}".format(codeframe.timestamp)}
220 for k, v in codeframe.items():235 info = {
221 if k != "timestamp":236 "label": codeframe.label,
222 ET.SubElement(event, k).text = str(v)237 "f_lineno": codeframe.frame.f_lineno,
238 "f_back": codeframe.frame.f_back,
239 "f_locals": codeframe.frame.f_locals,
240 "co_name": codeframe.code.co_name,
241 "co_argcount": codeframe.code.co_argcount,
242 "co_filename": codeframe.code.co_filename,
243 "co_names": codeframe.code.co_names,
244 "co_varnames": codeframe.code.co_varnames,
245 }
246 if codeframe.return_value is not None:
247 info["return_value"] = codeframe.return_value
248 for k, v in info.items():
249 ET.SubElement(event, k).text = str(v)
223 ET.SubElement(event, "type").text = event_type250 ET.SubElement(event, "type").text = event_type
224 ET.SubElement(event, "call_string").text = codeframe.call_string()251 ET.SubElement(event, "call_string").text = codeframe.call_string()
225 ET.SubElement(event, "short_call_string").text = codeframe.short_call_string()252 ET.SubElement(event, "short_call_string").text = codeframe.short_call_string()
226253
=== modified file 'src/entrails/_trace.py'
--- src/entrails/_trace.py 2014-07-07 12:48:49 +0000
+++ src/entrails/_trace.py 2014-09-03 20:05:48 +0000
@@ -12,6 +12,7 @@
1212
13__all__ = (13__all__ = (
14 "Entrails",14 "Entrails",
15 "Event",
15)16)
1617
1718
@@ -19,29 +20,85 @@
19import os.path20import os.path
20import sys21import sys
21import time22import time
23import types
2224
23from . import _filter25from . import _filter
24from . import _utils26from . import _utils
2527
2628
27class _Frame(dict):29class Event(object):
28 """30 """
29 Holds information about a frame.31 Holds information about a traced event.
3032
31 Use as a dict to access the important bits of the frame and arg33 Attributes:
32 parameters, along with timestamp and label. Also has convienience34 arg: Additional argument passed to the sys.settrace-installed function.
33 methods to generate function call string and class info. This is35 code: Code object being executed.
34 supplied to an output adapter's call(), return() and exception()36 frame: Frame object for the executing code.
35 methods.37 label: String passed when event was created.
36 """38 return_value: For function exit events the value returned, otherwise
39 None.
40 timestamp: Time, in seconds relative to initialisation, at which the
41 event occurred.
42
43 Instances are supplied to an output adapter's call(), return() and
44 exception() methods.
45
46 """
47
48 def __init__(self, frame, event, arg,
49 label, timestamp):
50 """
51 Initialise this event.
52
53 frame:
54 Frame object passed to trace function.
55 event:
56 Event string passed to trace function.
57 arg:
58 Final argument passed to trace function.
59 label:
60 String identifying the context for the event.
61 timestamp:
62 Time, in seconds relative to initialisation, at which the event
63 occurred.
64
65 """
66 self.arg = arg
67 self.code = frame.f_code
68 self.frame = frame
69 self.label = label
70 self.timestamp = timestamp
71
72 self.return_value = (arg if event == "return" else None)
73
74 def basename(self):
75 """Returns the name of the function for this event's frame."""
76 cls, method = self._retrieve_cls_method()
77 if method is not None:
78 try:
79 name = method.__qualname__
80 except AttributeError:
81 # Old version, < 3.3.
82 name = "{}.{}".format(cls.__name__, method.__name__)
83 else:
84 name = self.frame.f_code.co_name
85
86 return name
87
88 def qualname(self):
89 """Returns the fully-qualified function name for this event's frame."""
90 return "{}.{}".format(self.frame.f_globals["__name__"],
91 self.basename())
92
37 def parameters(self):93 def parameters(self):
38 """94 """
39 Returns dict of form {parameter_name: argument_value}.95 Returns dict of form {parameter_name: argument_value}.
40 """96 """
41 # We rely on CPython putting arguments at start of f_locals (in reverse97 # We rely on CPython putting arguments at start of f_locals (in reverse
42 # order for some reason...). This is undocumented, but consistent.98 # order for some reason...). This is undocumented, but consistent.
43 parameters = [(k, v) for k, v in self["f_locals"].items()99 parameters = [(k, v)
44 if k in self["co_varnames"][0:self["co_argcount"]]]100 for k, v in self.frame.f_locals.items()
101 if k in self.code.co_varnames[0:self.code.co_argcount]]
45 parameters.reverse()102 parameters.reverse()
46 return collections.OrderedDict(parameters)103 return collections.OrderedDict(parameters)
47104
@@ -79,14 +136,13 @@
79136
80 Only designed for 'call' event (i.e. function entry)137 Only designed for 'call' event (i.e. function entry)
81 """138 """
139 output = '{}({})'.format(self.qualname(),
140 ', '.join(self.string_arguments()))
82 if self.is_method():141 if self.is_method():
83 return '{0}.{1}({2})'.format(self.instance_name(), self['co_name'],142 output += ' on instance {}'.format(self.instance_name())
84 ', '.join(self.string_arguments()))143 return output
85 else:
86 return '{0}({1})'.format(self['co_name'],
87 ', '.join(self.string_arguments()))
88144
89 def short_call_string(self, length=40):145 def short_call_string(self, length=80):
90 """146 """
91 Returns a pretty string of the function call, shortened to at147 Returns a pretty string of the function call, shortened to at
92 most length characters.148 most length characters.
@@ -95,7 +151,7 @@
95 """151 """
96 return _utils.pretty_resize_string(self.call_string(), length)152 return _utils.pretty_resize_string(self.call_string(), length)
97153
98 def short_return_value(self, length=40):154 def short_return_value(self, length=80):
99 """155 """
100 Returns a pretty string representation of the return value,156 Returns a pretty string representation of the return value,
101 shortened to at most length characters.157 shortened to at most length characters.
@@ -103,7 +159,7 @@
103 Only designed for 'call' event (i.e. function entry)159 Only designed for 'call' event (i.e. function entry)
104 """160 """
105 try:161 try:
106 return _utils.pretty_resize_string(repr(self['return_value']), length)162 return _utils.pretty_resize_string(repr(self.return_value), length)
107 except:163 except:
108 return _utils.pretty_resize_string('<repr error>', length)164 return _utils.pretty_resize_string('<repr error>', length)
109165
@@ -120,7 +176,7 @@
120 """176 """
121 Nice filename, without directory segment.177 Nice filename, without directory segment.
122 """178 """
123 return os.path.basename(self["co_filename"])179 return os.path.basename(self.code.co_filename)
124180
125 def is_method(self):181 def is_method(self):
126 """182 """
@@ -131,7 +187,7 @@
131187
132 TODO: What happens for class methods/other decorated methods?188 TODO: What happens for class methods/other decorated methods?
133 """189 """
134 if self['co_varnames'] and self['co_varnames'][0] == 'self':190 if self.code.co_varnames and self.code.co_varnames[0] == 'self':
135 return True191 return True
136 return False192 return False
137193
@@ -141,9 +197,12 @@
141 Only designed for 'call' event (i.e. function entry)197 Only designed for 'call' event (i.e. function entry)
142 We cache value after first proper lookup.198 We cache value after first proper lookup.
143 """199 """
144 if 'instance_name' not in self:200 try:
145 self['instance_name'] = self._retrieve_instance_name()201 name = self._instance_name
146 return self['instance_name']202 except AttributeError:
203 name = self._retrieve_instance_name()
204 self._instance_name = name
205 return name
147206
148 def _retrieve_instance_name(self):207 def _retrieve_instance_name(self):
149 """208 """
@@ -153,22 +212,59 @@
153 try:212 try:
154 # This is confusing. We go into the previous (parent) frame and213 # This is confusing. We go into the previous (parent) frame and
155 # find the local variable name in that pointing to a our class.214 # find the local variable name in that pointing to a our class.
156 for k, v in list(self['f_back'].f_locals.items()):215 for k, v in list(self.frame.f_back.f_locals.items()):
157 if v == self['f_locals']['self']:216 if v == self.frame.f_locals['self']:
158 return k217 return k
159 except:218 except:
160 return '<lookup error>'219 return '<lookup error>'
161220
162 def class_name(self):221 def method(self):
163 """222 """
164 Class name of method, or empty string if not method.223 Attempt to find the method corresponding to this event, if any.
165 Only designed for 'call' event (i.e. function entry)224
166 Should only be called on methods.225 On success, returns the function found. If the frame doesn't look like
167 """226 it corresponds to a method, `None` is returned.
168 try:227
169 return self["f_locals"]["self"].__class__.__name__228 """
170 except:229 return self._retrieve_cls_method()[1]
171 return '<lookup error>'230
231 def _retrieve_cls_method(self):
232 """
233 Attempt to find the method and class corresponding to this event.
234
235 On success returns a class, method tuple, where the method is an
236 attribute on the class returned (helpful for <3.3 where __qualname__
237 isn't available).
238
239 If the event doesn't look like it corresponds to a method, returns
240 None, None.
241
242 """
243 if not self.code.co_varnames or self.code.co_varnames[0] != "self":
244 return None, None
245
246 meth_self = self.frame.f_locals["self"]
247 meth_name = self.code.co_name
248 is_unbound_method = False
249
250 # Alternative approaches:
251 # - Source code parsing via inspect.
252 # - gc.get_referrers
253 # - ?
254 #
255 # GC is plausible (but won't scale very well with number of objects).
256
257 for cls in type(meth_self).mro():
258 try:
259 candidate = getattr(cls, meth_name)
260 except AttributeError:
261 continue
262
263 if callable(candidate):
264 return cls, candidate
265
266 else:
267 return None, None
172268
173269
174class Entrails(object):270class Entrails(object):
@@ -184,7 +280,9 @@
184280
185 def __init__(self,281 def __init__(self,
186 label='<untitled>',282 label='<untitled>',
187 filters=(_filter.LibraryFilter(), _filter.SelfFilter())):283 filters=None):
284 if filters is None:
285 filters = (_filter.LibraryFilter(), _filter.SelfFilter())
188 self._outputs = []286 self._outputs = []
189 self._init_time = time.time()287 self._init_time = time.time()
190 self._label = label288 self._label = label
@@ -198,19 +296,10 @@
198 return True296 return True
199 return False297 return False
200298
201 def _trace_function(self, trace, event, arg):299 def _trace_function(self, frame, event, arg):
202 # Build Frame300 obj = Event(frame, event, arg,
203 obj = _Frame()301 self._label,
204 obj["label"] = self._label302 time.time() - self._init_time)
205 obj["timestamp"] = time.time() - self._init_time
206 obj["f_lineno"] = trace.f_lineno
207 obj["f_back"] = trace.f_back
208 obj["f_locals"] = trace.f_locals
209 obj["co_name"] = trace.f_code.co_name
210 obj["co_argcount"] = trace.f_code.co_argcount
211 obj["co_filename"] = trace.f_code.co_filename
212 obj["co_names"] = trace.f_code.co_names
213 obj["co_varnames"] = trace.f_code.co_varnames
214303
215 # Filter304 # Filter
216 # If this object is to be discarded, return trace_function to305 # If this object is to be discarded, return trace_function to
@@ -218,11 +307,6 @@
218 if not self.filter(obj):307 if not self.filter(obj):
219 return self._trace_function308 return self._trace_function
220309
221 # Possibly add return_value. If event is non-return, arg isn't really
222 # that useful.
223 if event is 'return':
224 obj["return_value"] = arg
225
226 if event in self.event_names:310 if event in self.event_names:
227 for output in self._outputs:311 for output in self._outputs:
228 #try:312 #try:
229313
=== modified file 'src/trace.py'
--- src/trace.py 2014-07-07 12:48:49 +0000
+++ src/trace.py 2014-09-03 20:05:48 +0000
@@ -47,7 +47,7 @@
47 first=True47 first=True
48 for line in fileinput.input(filename, inplace=1, backup=".backup"):48 for line in fileinput.input(filename, inplace=1, backup=".backup"):
49 if first and line.startswith("#!/"):49 if first and line.startswith("#!/"):
50 print(line, end=' ')50 print(line, end='')
51 continue51 continue
52 if first:52 if first:
53 print(MARKER)53 print(MARKER)
@@ -61,7 +61,7 @@
61 print("_entrails_t.start_trace()")61 print("_entrails_t.start_trace()")
62 print(ENDMARKER)62 print(ENDMARKER)
63 first = False63 first = False
64 print(line, end=' ')64 print(line, end='')
65 fileinput.close()65 fileinput.close()
6666
6767
@@ -131,7 +131,7 @@
131 if line.startswith(MARKER):131 if line.startswith(MARKER):
132 extra = True132 extra = True
133 if not extra:133 if not extra:
134 print(line, end=' ')134 print(line, end='')
135 if line.startswith(ENDMARKER):135 if line.startswith(ENDMARKER):
136 extra = False136 extra = False
137 fileinput.close()137 fileinput.close()

Subscribers

People subscribed via source and target branches