Merge lp:~pconnell/entrails/modularise into lp:entrails
- modularise
- Merge into trunk
Proposed by
Phil Connell
Status: | Merged |
---|---|
Approved by: | Martin Morrison |
Approved revision: | 21 |
Merged at revision: | 19 |
Proposed branch: | lp:~pconnell/entrails/modularise |
Merge into: | lp:entrails |
Diff against target: |
1192 lines (+613/-521) 6 files modified
src/entrails/__init__.py (+26/-516) src/entrails/_filter.py (+60/-0) src/entrails/_output.py (+235/-0) src/entrails/_trace.py (+258/-0) src/entrails/_utils.py (+29/-0) src/trace.py (+5/-5) |
To merge this branch: | bzr merge lp:~pconnell/entrails/modularise |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Morrison | Approve | ||
Review via email: mp+225789@code.launchpad.net |
This proposal supersedes a proposal from 2014-07-04.
Commit message
Split monolithic entrails module into a package.
Description of the change
Split monolithic entrails module into a package.
Tweaked naming slightly (e.g. entrails.OutputHTML -> entrails.
A few other trivial changes along the way e.g. added a couple of docstrings/__all__s
New modules:
_trace.py: core tracing functionality
_utils.py: mostly-empty dumping ground for util functions
_filter.py: filters
_output.py: output generators
__init__.py: pull everything together into the public entrails namespace
Also updated trace.py to handle the new namespace.
To post a comment you must log in.
Revision history for this message
Martin Morrison (isoschiz) : Posted in a previous version of this proposal | # |
Revision history for this message
Phil Connell (pconnell) wrote : Posted in a previous version of this proposal | # |
Revision history for this message
Martin Morrison (isoschiz) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'src/entrails' |
2 | === renamed file 'src/entrails.py' => 'src/entrails/__init__.py' |
3 | --- src/entrails.py 2013-03-27 09:10:41 +0000 |
4 | +++ src/entrails/__init__.py 2014-07-07 09:01:02 +0000 |
5 | @@ -1,531 +1,41 @@ |
6 | -#!/usr/bin/env python2.7 |
7 | # ----------------------------------------------------------------------------- |
8 | -# EnTrails - Python Execution Tracing Module |
9 | -# Copyright 2012, Ensoft Ltd. |
10 | +# EnTrails - Python Execution Tracing |
11 | +# Copyright 2012, 2014, Ensoft Ltd. |
12 | # Created by Ben Eills |
13 | # ----------------------------------------------------------------------------- |
14 | |
15 | """ |
16 | -Single-file module, containing main EnTrails components and several output |
17 | - modules/filters. |
18 | +Trace execution of Python programs. |
19 | + |
20 | Outputs may be loaded/unloaded during execution and tracing may be toggled |
21 | - during runtime. |
22 | +during runtime. |
23 | |
24 | Example Usage as a module: |
25 | - t = Entrails('mylabel') |
26 | - o = OutputLogFile('/tmp/log') |
27 | - t.add_otuput(o) |
28 | + import entrails |
29 | + t = entrails.Entrails('mylabel') |
30 | + o = entrails.LogFileOutput('/tmp/log') |
31 | + t.add_output(o) |
32 | t.start_trace() |
33 | do_stuff() |
34 | t.end_trace() |
35 | |
36 | Consider using trace.py to add EnTrails tracing to an existing Python program |
37 | - without having to add the boilerplate code yourself. That convenience script |
38 | - should be distributed with this source file. |
39 | +without having to add the boilerplate code yourself. That convenience script |
40 | +should be distributed with this source file. |
41 | + |
42 | """ |
43 | |
44 | -import os.path |
45 | -import sys |
46 | -import datetime |
47 | -import time |
48 | -import sys |
49 | -import xml.dom |
50 | -import xml.etree.ElementTree as ET |
51 | -from collections import OrderedDict |
52 | - |
53 | - |
54 | -def pretty_resize_string(string, length=25, cutoff='back', exact=False): |
55 | - """ |
56 | - Shortens or lengthens a string to be at most length characters long. |
57 | - cutoff should be 'front' or 'back', dictating where the ellipsis goes. |
58 | - exact toggles whitespace padding. |
59 | - """ |
60 | - if len(string) > length: |
61 | - cutoff_string = '{0}...' if cutoff is 'back' else '...{0}' |
62 | - return cutoff_string.format(string[3-length:]) |
63 | - else: |
64 | - exact_string = '{:>25}' if exact else '{0}' |
65 | - return exact_string.format(string) |
66 | - |
67 | - |
68 | -class FrameObject(dict): |
69 | - """ |
70 | - Holds information about the frame. |
71 | - |
72 | - Use as a dict to access the important bits of the frame and arg |
73 | - parameters, along with timestamp and label. Also has convienience |
74 | - methods to generate function call string and class info. This is |
75 | - supplied to an output adapter's call(), return() and exception() |
76 | - methods. |
77 | - """ |
78 | - def parameters(self): |
79 | - """ |
80 | - Returns dict of form {parameter_name: argument_value}. |
81 | - """ |
82 | - # We rely on CPython putting arguments at start of f_locals (in reverse |
83 | - # order for some reason...). This is undocumented, but consistent. |
84 | - parameters = [(k, v) for k, v in self["f_locals"].iteritems() |
85 | - if k in self["co_varnames"][0:self["co_argcount"]]] |
86 | - parameters.reverse() |
87 | - return OrderedDict(parameters) |
88 | - |
89 | - def arguments(self): |
90 | - """ |
91 | - Returns argument list, possibly excluding the first 'self' arg |
92 | - if the function is a method. |
93 | - |
94 | - Only designed for 'call' event (i.e. function entry) |
95 | - """ |
96 | - # Possibly chop off first self argument |
97 | - ignorer = 0 if not self.is_method() else 1 |
98 | - return self.parameters().values()[ignorer:] |
99 | - |
100 | - def string_arguments(self): |
101 | - """ |
102 | - Returns list of nice string argument representations. |
103 | - |
104 | - If the argument object encounters an error during repr() call |
105 | - (some standard library classes can do this under certain conditions) |
106 | - fail cleanly. |
107 | - """ |
108 | - strings = [] |
109 | - for arg in self.arguments(): |
110 | - try: |
111 | - strings.append(repr(arg)) |
112 | - except: |
113 | - strings.append('<repr error>') |
114 | - return strings |
115 | - |
116 | - def call_string(self): |
117 | - """ |
118 | - Returns a pretty string representation of the function call built |
119 | - from function name and args, nicely formatted/abbrieviated. |
120 | - |
121 | - Only designed for 'call' event (i.e. function entry) |
122 | - """ |
123 | - if self.is_method(): |
124 | - return '{0}.{1}({2})'.format(self.instance_name(), self['co_name'], |
125 | - ', '.join(self.string_arguments())) |
126 | - else: |
127 | - return '{0}({1})'.format(self['co_name'], |
128 | - ', '.join(self.string_arguments())) |
129 | - |
130 | - def short_call_string(self, length=40): |
131 | - """ |
132 | - Returns a pretty string of the function call, shortened to at |
133 | - most length characters. |
134 | - |
135 | - Only designed for 'call' event (i.e. function entry) |
136 | - """ |
137 | - return pretty_resize_string(self.call_string(), length) |
138 | - |
139 | - def short_return_value(self, length=40): |
140 | - """ |
141 | - Returns a pretty string representation of the return value, |
142 | - shortened to at most length characters. |
143 | - |
144 | - Only designed for 'call' event (i.e. function entry) |
145 | - """ |
146 | - try: |
147 | - return pretty_resize_string(repr(self['return_value']), length) |
148 | - except: |
149 | - return pretty_resize_string('<repr error>', length) |
150 | - |
151 | - # Do we close quotation marks in string return values? |
152 | - # if len(repr(rv)) > 40: |
153 | - # if rv.__class__.__name__ == 'str': |
154 | - # return repr(rv)[0:length-4]+"...'" |
155 | - # else: |
156 | - # return repr(rv)[0:length-3]+"..." |
157 | - # else: |
158 | - # return repr(rv) |
159 | - |
160 | - def nice_filename(self): |
161 | - """ |
162 | - Nice filename, without directory segment. |
163 | - """ |
164 | - return os.path.basename(self["co_filename"]) |
165 | - |
166 | - |
167 | - def is_method(self): |
168 | - """ |
169 | - True if function is an instance method (and the programmer |
170 | - did not defy all convention by abandonning the use of 'self' |
171 | - as the instance pointer). |
172 | - Only designed for 'call' event (i.e. function entry) |
173 | - |
174 | - TODO: What happens for class methods/other decorated methods? |
175 | - """ |
176 | - if self['co_varnames'] and self['co_varnames'][0] == 'self': |
177 | - return True |
178 | - return False |
179 | - |
180 | - def instance_name(self): |
181 | - """ |
182 | - Class instance name of method, or empty string if not method. |
183 | - Only designed for 'call' event (i.e. function entry) |
184 | - We cache value after first proper lookup. |
185 | - """ |
186 | - if 'instance_name' not in self: |
187 | - self['instance_name'] = self._retrieve_instance_name() |
188 | - return self['instance_name'] |
189 | - |
190 | - def _retrieve_instance_name(self): |
191 | - """ |
192 | - Get class instance name by performance-intensive lookup. |
193 | - Should only be called on methods. |
194 | - """ |
195 | - try: |
196 | - # This is confusing. We go into the previous (parent) frame and |
197 | - # find the local variable name in that pointing to a our class. |
198 | - for k, v in self['f_back'].f_locals.items(): |
199 | - if v == self['f_locals']['self']: |
200 | - return k |
201 | - except: |
202 | - return '<lookup error>' |
203 | - |
204 | - def class_name(self): |
205 | - """ |
206 | - Class name of method, or empty string if not method. |
207 | - Only designed for 'call' event (i.e. function entry) |
208 | - Should only be called on methods. |
209 | - """ |
210 | - try: |
211 | - return self["f_locals"]["self"].__class__.__name__ |
212 | - except: |
213 | - return '<lookup error>' |
214 | - |
215 | - |
216 | -class Filter(object): |
217 | - """ |
218 | - Returns True if object remains unfiltered. |
219 | - Must be very fast at executing, since we deal with so many objects. |
220 | - """ |
221 | - def filter(self, frameobjects): |
222 | - """ |
223 | - Trivial filter (allowing all events to pass through) |
224 | - """ |
225 | - return frameobjects |
226 | - |
227 | -class LibraryFilter(Filter): |
228 | - """ |
229 | - Filters out major 3rd party libraries and Standard Library. |
230 | - """ |
231 | - matches = ("/usr/lib", "/usr/local/lib", "/usr/lib64", "/twisted", "/zope") |
232 | - |
233 | - def filter(self, frameobject): |
234 | - for m in self.matches: |
235 | - if m in frameobject['co_filename']: |
236 | - return False |
237 | - return True |
238 | - |
239 | -class SelfFilter(Filter): |
240 | - """ |
241 | - Filter out EnTrails events. |
242 | - |
243 | - Performance-wise, it might be better to have this hardcoded below |
244 | - to save a couple of JMP statements, but architecturally this is |
245 | - where it goes. |
246 | - """ |
247 | - matches = ("remove_output", "end_trace") |
248 | - |
249 | - def filter(self, frameobject): |
250 | - for m in self.matches: |
251 | - if m in frameobject['co_name']: |
252 | - return False |
253 | - return True |
254 | - |
255 | -### TODO BELOW LINE |
256 | - |
257 | -class Entrails(object): |
258 | - """ |
259 | - Implements entrails functionality to monitor program execution flow. |
260 | - |
261 | - Instantiate at in process to be traced, add outputs and call start_trace(). |
262 | - Run end_trace() to safely close outputs and stop tracing. |
263 | - """ |
264 | - event_names = { 'call' : 'enter', |
265 | - 'return' : 'exit', |
266 | - 'exception' : 'exception' } |
267 | - |
268 | - def __init__(self, |
269 | - label='<untitled>', filters=(LibraryFilter(), SelfFilter())): |
270 | - self._outputs = [] |
271 | - self._init_time = time.time() |
272 | - self._label = label |
273 | - self._filters = filters |
274 | - |
275 | - def filter(self, obj): |
276 | - """ |
277 | - Returns True if object remains unfiltered, False otherise. |
278 | - """ |
279 | - if all(f.filter(obj) for f in self._filters): |
280 | - return True |
281 | - return False |
282 | - |
283 | - def _trace_function(self, trace, event, arg): |
284 | - # Build FrameObject |
285 | - obj = FrameObject() |
286 | - obj["label"] = self._label |
287 | - obj["timestamp"] = time.time() - self._init_time |
288 | - obj["f_lineno"] = trace.f_lineno |
289 | - obj["f_back"] = trace.f_back |
290 | - obj["f_locals"] = trace.f_locals |
291 | - obj["co_name"] = trace.f_code.co_name |
292 | - obj["co_argcount"] = trace.f_code.co_argcount |
293 | - obj["co_filename"] = trace.f_code.co_filename |
294 | - obj["co_names"] = trace.f_code.co_names |
295 | - obj["co_varnames"] = trace.f_code.co_varnames |
296 | - |
297 | - # Filter |
298 | - # If this object is to be discarded, return trace_function to |
299 | - # indicate that we wish to continue tracing. |
300 | - if not self.filter(obj): |
301 | - return self._trace_function |
302 | - |
303 | - # Possibly add return_value. If event is non-return, arg isn't really |
304 | - # that useful. |
305 | - if event is 'return': |
306 | - obj["return_value"] = arg |
307 | - |
308 | - if event in self.event_names: |
309 | - for output in self._outputs: |
310 | - #try: |
311 | - getattr(output, self.event_names[event])(obj) |
312 | - return self._trace_function |
313 | - |
314 | - def add_output(self, output_object): |
315 | - """ |
316 | - Adds output adapter, accepting argument of type EntrailsOutput. |
317 | - """ |
318 | - self._outputs.append(output_object) |
319 | - |
320 | - def remove_output(self, output_object): |
321 | - """ |
322 | - Cease sending tracing data to a particular output. |
323 | - Removes from internal list and runs output.__del__()@@@ |
324 | - """ |
325 | - output_object.close() |
326 | - if output_object in self._outputs: |
327 | - self._outputs.remove(output_object) |
328 | - |
329 | - def start_trace(self): |
330 | - """ |
331 | - Start tracing. |
332 | - """ |
333 | - sys.settrace(self._trace_function) |
334 | - def end_trace(self): |
335 | - """ |
336 | - Stop tracing. Safely removes all outputs. |
337 | - """ |
338 | - for o in self._outputs: |
339 | - self.remove_output(o) |
340 | - sys.settrace(None) |
341 | - |
342 | -class EntrailsOutput(object): |
343 | - """ |
344 | - Skeleton output adapter class; methods called for function |
345 | - entry/exit and exceptions. |
346 | - |
347 | - Use as template to implement custom output adapters. |
348 | - """ |
349 | - def __init__(self): |
350 | - pass |
351 | - def close(self): |
352 | - pass |
353 | - def enter(self, o): |
354 | - pass |
355 | - def exit(self, o): |
356 | - pass |
357 | - def exception(self, o): |
358 | - pass |
359 | - def logging(self, o, event_type_string): |
360 | - pass |
361 | - |
362 | -class OutputPlainText(EntrailsOutput): |
363 | - """ |
364 | - Output adapter generating plaintext of variable verbosity, one |
365 | - event per line. |
366 | - |
367 | - TODO: printf formatting, verbosity |
368 | - """ |
369 | - def __init__(self, verbosity, prefix_func=None, depth_spacer=" ", |
370 | - enter_func=None, exit_func=None, exception_func=None): |
371 | - self.depth_spacer = depth_spacer |
372 | - self.safe_write("Starting trace in program\n") |
373 | - self.depth = 0 |
374 | - self.prefix_func = prefix_func or self._default_prefix_func |
375 | - self.enter_func = enter_func or self._default_enter_func |
376 | - self.exit_func = exit_func or self._default_exit_func |
377 | - self.exception_func = exception_func or self._default_exception_func |
378 | - |
379 | - def close(self): |
380 | - if not self.f.closed: |
381 | - self.f.close() |
382 | - |
383 | - def safe_write(self, data): |
384 | - if not self.f.closed: |
385 | - self.f.write(data) |
386 | - |
387 | - def _default_enter_func(self, o): |
388 | - return "Call " + o.short_call_string() |
389 | - |
390 | - def _default_exit_func(self, o): |
391 | - return "Return from {0} with value {1}".format(o["co_name"], |
392 | - o.short_return_value()) |
393 | - |
394 | - def _default_exception_func(self, o): |
395 | - return "Encountered exception" #@@@ more info TODO |
396 | - |
397 | - def _default_prefix_func(self, o): |
398 | - return ("[%s][%03d:%s]" + " "*self.depth) % (o["label"], |
399 | - o["f_lineno"], |
400 | - pretty_resize_string(o.nice_filename(), 15, exact=True)) |
401 | - |
402 | - def enter(self, o): |
403 | - self.depth += 1 |
404 | - self.safe_write(self.prefix_func(o) + self.enter_func(o) + "\n") |
405 | - return None |
406 | - |
407 | - def exit(self, o): |
408 | - self.safe_write(self.prefix_func(o) + self.exit_func(o) + "\n") |
409 | - self.depth -= 1 |
410 | - return None |
411 | - |
412 | - def exception(self, o): |
413 | - self.safe_write(self.prefix_func(o) + self.exception_func(o) + "\n") |
414 | - return None |
415 | - |
416 | - def logging(self, o, event_type_string): |
417 | - self.safe_write("ERROR TRACING with event: %s on line %d in %s\n" % ( |
418 | - event_type_string, o["f_lineno"], o["co_filename"])) |
419 | - return None |
420 | - |
421 | - |
422 | -class OutputLogFile(OutputPlainText): |
423 | - """ |
424 | - Convienience function to send plaintext output to a file. |
425 | - """ |
426 | - def __init__(self, filename, verbosity, prefix_func=None, depth_spacer=" ", |
427 | - enter_func=None, exit_func=None, exception_func=None): |
428 | - self.f = open(filename, 'a', 1) #line-buffering |
429 | - super(OutputLogFile, self).__init__(verbosity, prefix_func=prefix_func, |
430 | - depth_spacer=depth_spacer, |
431 | - enter_func=enter_func, |
432 | - exit_func=exit_func, |
433 | - exception_func=exception_func) |
434 | - |
435 | - |
436 | -class OutputConsole(OutputPlainText): |
437 | - """ |
438 | - Convienience function to send plaintext output to stdout. |
439 | - """ |
440 | - def __init__(self, filename, verbosity, prefix_func=None, depth_spacer=" ", |
441 | - enter_func=None, exit_func=None, exception_func=None): |
442 | - self.f = sys.stdout |
443 | - super(OutputConsole, self).__init__(verbosity, prefix_func=prefix_func, |
444 | - depth_spacer=depth_spacer, |
445 | - enter_func=enter_func, |
446 | - exit_func=exit_func, |
447 | - exception_func=exception_func) |
448 | - |
449 | - |
450 | -class OutputDebug(EntrailsOutput): |
451 | - """ |
452 | - Displays all output data with little formatting. |
453 | - """ |
454 | - def __init__(self): |
455 | - print "Entrails debug output started on {0}".format( |
456 | - datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) |
457 | - |
458 | - def close(self): |
459 | - print "Entrails debug output ended on {0}".format( |
460 | - datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) |
461 | - |
462 | - def enter(self, o): |
463 | - print "FUNCTION CALL: " + o.call_string() |
464 | - for k, v in o.iteritems(): |
465 | - print "\t{0}:{1}".format(k, str(v)) |
466 | - return None |
467 | - |
468 | - def exit(self, o): |
469 | - print "FUNCTION EXIT" |
470 | - for k, v in o.iteritems(): |
471 | - print "\t{0}:{1}".format(k, str(v)) |
472 | - return None |
473 | - |
474 | - def exception(self, o): |
475 | - print "EXCEPTION on line {0} in {1}".format(o["f_lineno"], |
476 | - o["co_filename"]) |
477 | - for k, v in o.iteritems(): |
478 | - print k + ": " + str(v) |
479 | - return None |
480 | - |
481 | - def logging(self, o, event_type_string): |
482 | - print "ERROR TRACING with event: {0} on line {1} in {2}".format(event_type_string, |
483 | - o["f_lineno"], |
484 | - o["co_filename"]) |
485 | - return None |
486 | - |
487 | - |
488 | -class OutputHTML(EntrailsOutput): |
489 | - """ |
490 | - Not fully implemented. Basic HTML output. |
491 | - """ |
492 | - def __init__(self, filename, verbosity): |
493 | - self.depth = 0 |
494 | - self.f = open(filename, 'a', 1) #line-buffering |
495 | - self.f.write("<html><body><h1>Trace of program started on {0}</h1>\n".format(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) |
496 | - |
497 | - def __del__(self): |
498 | - self.f.write("\n</body></html>\n") |
499 | - self.f.close() |
500 | - |
501 | - def enter(self, codeframe): |
502 | - self.depth += 1 |
503 | - self.f.write(" > "*self.depth + "<b>Now entering:</b> " + codeframe["name"] + "<br />\n") |
504 | - return None |
505 | - |
506 | - def exit(self, codeframe): |
507 | - self.depth -= 1 |
508 | - self.f.write(" > "*(self.depth+1) + "<b>and exiting:</b> " + codeframe["name"] + " with return value " + str(codeframe["return_value"]) + "<br />\n") |
509 | - return None |
510 | - |
511 | - def exception(self, o): |
512 | - self.f.write("<h3>Encountered exception!</h3>") |
513 | - return None |
514 | - |
515 | - def logging(self, o, event_type_string): |
516 | - self.f.write("<h4>ERROR TRACING with event: %s on line %d in %s</h4>" % (event_type_string, o["f_lineno"], o["co_filename"])) |
517 | - return None |
518 | - |
519 | - |
520 | -class OutputXML(EntrailsOutput): |
521 | - def __init__(self, filename): |
522 | - self.filename = filename |
523 | - self.root = ET.Element("data") |
524 | - |
525 | - def close(self): |
526 | - tree = ET.ElementTree(self.root) |
527 | - tree.write(self.filename, xml_declaration=True, method="xml") |
528 | - |
529 | - def write_out(self, codeframe, event_type): |
530 | - event = ET.SubElement(self.root, "event") |
531 | - event.attrib = {"timestamp" : "{0:f}".format(codeframe["timestamp"])} |
532 | - for k, v in codeframe.iteritems(): |
533 | - if k != "timestamp": |
534 | - ET.SubElement(event, k).text = str(v) |
535 | - ET.SubElement(event, "type").text = event_type |
536 | - ET.SubElement(event, "call_string").text = codeframe.call_string() |
537 | - ET.SubElement(event, "short_call_string").text = codeframe.short_call_string() |
538 | - ET.SubElement(event, "short_return_value").text = codeframe.short_return_value() |
539 | - ET.SubElement(event, "nice_filename").text = codeframe.nice_filename() |
540 | - |
541 | - def enter(self, codeframe): |
542 | - self.write_out(codeframe, "enter") |
543 | - |
544 | - def exit(self, codeframe): |
545 | - self.write_out(codeframe, "exit") |
546 | - |
547 | - def exception(self, codeframe): |
548 | - self.write_out(codeframe, "exception") |
549 | +from . import _filter |
550 | +from . import _output |
551 | +from . import _trace |
552 | + |
553 | +__all__ = ( |
554 | + _filter.__all__ + |
555 | + _output.__all__ + |
556 | + _trace.__all__ |
557 | +) |
558 | + |
559 | +from ._filter import * |
560 | +from ._output import * |
561 | +from ._trace import * |
562 | + |
563 | |
564 | === added file 'src/entrails/_filter.py' |
565 | --- src/entrails/_filter.py 1970-01-01 00:00:00 +0000 |
566 | +++ src/entrails/_filter.py 2014-07-07 09:01:02 +0000 |
567 | @@ -0,0 +1,60 @@ |
568 | +# ----------------------------------------------------------------------------- |
569 | +# _filter.py - Tracing filters. |
570 | +# |
571 | +# 2012, Ben Eills |
572 | +# |
573 | +# Copyright 2012, 2014, Ensoft Ltd. |
574 | +# ----------------------------------------------------------------------------- |
575 | + |
576 | +"""Tracing filters.""" |
577 | + |
578 | +__all__ = ( |
579 | + "EntrailsFilter", |
580 | + "LibraryFilter", |
581 | + "SelfFilter", |
582 | +) |
583 | + |
584 | + |
585 | +class EntrailsFilter(object): |
586 | + """ |
587 | + Base class for filters. |
588 | + |
589 | + Returns True if object remains unfiltered. |
590 | + Must be very fast at executing, since we deal with so many objects. |
591 | + """ |
592 | + def filter(self, frameobjects): |
593 | + """ |
594 | + Trivial filter (allowing all events to pass through) |
595 | + """ |
596 | + return frameobjects |
597 | + |
598 | + |
599 | +class LibraryFilter(EntrailsFilter): |
600 | + """ |
601 | + Filters out major 3rd party libraries and Standard Library. |
602 | + """ |
603 | + matches = ("/usr/lib", "/usr/local/lib", "/usr/lib64", "/twisted", "/zope") |
604 | + |
605 | + def filter(self, frameobject): |
606 | + for m in self.matches: |
607 | + if m in frameobject['co_filename']: |
608 | + return False |
609 | + return True |
610 | + |
611 | + |
612 | +class SelfFilter(EntrailsFilter): |
613 | + """ |
614 | + Filter out EnTrails events. |
615 | + |
616 | + Performance-wise, it might be better to have this hardcoded below |
617 | + to save a couple of JMP statements, but architecturally this is |
618 | + where it goes. |
619 | + """ |
620 | + matches = ("remove_output", "end_trace") |
621 | + |
622 | + def filter(self, frameobject): |
623 | + for m in self.matches: |
624 | + if m in frameobject['co_name']: |
625 | + return False |
626 | + return True |
627 | + |
628 | |
629 | === added file 'src/entrails/_output.py' |
630 | --- src/entrails/_output.py 1970-01-01 00:00:00 +0000 |
631 | +++ src/entrails/_output.py 2014-07-07 09:01:02 +0000 |
632 | @@ -0,0 +1,235 @@ |
633 | +# ----------------------------------------------------------------------------- |
634 | +# _output.py - Output generators. |
635 | +# |
636 | +# 2012, Ben Eills |
637 | +# |
638 | +# Copyright 2012, 2014, Ensoft Ltd. |
639 | +# ----------------------------------------------------------------------------- |
640 | + |
641 | +"""Output generators.""" |
642 | + |
643 | +__all__ = ( |
644 | + "EntrailsOutput", |
645 | + "ConsoleOutput", |
646 | + "DebugOutput", |
647 | + "HTMLOutput", |
648 | + "LogFileOutput", |
649 | + "PlainTextOutput", |
650 | + "XMLOutput", |
651 | +) |
652 | + |
653 | + |
654 | +import datetime |
655 | +import sys |
656 | + |
657 | +from . import _utils |
658 | + |
659 | + |
660 | +class EntrailsOutput(object): |
661 | + """ |
662 | + Base output adapter class. |
663 | + |
664 | + Use as template to implement custom output adapters. |
665 | + """ |
666 | + def __init__(self): |
667 | + pass |
668 | + def close(self): |
669 | + pass |
670 | + def enter(self, o): |
671 | + pass |
672 | + def exit(self, o): |
673 | + pass |
674 | + def exception(self, o): |
675 | + pass |
676 | + def logging(self, o, event_type_string): |
677 | + pass |
678 | + |
679 | + |
680 | +class PlainTextOutput(EntrailsOutput): |
681 | + """ |
682 | + Output adapter generating plaintext of variable verbosity, one |
683 | + event per line. |
684 | + |
685 | + TODO: printf formatting, verbosity |
686 | + """ |
687 | + def __init__(self, verbosity, prefix_func=None, depth_spacer=" ", |
688 | + enter_func=None, exit_func=None, exception_func=None): |
689 | + self.depth_spacer = depth_spacer |
690 | + self.safe_write("Starting trace in program\n") |
691 | + self.depth = 0 |
692 | + self.prefix_func = prefix_func or self._default_prefix_func |
693 | + self.enter_func = enter_func or self._default_enter_func |
694 | + self.exit_func = exit_func or self._default_exit_func |
695 | + self.exception_func = exception_func or self._default_exception_func |
696 | + |
697 | + def close(self): |
698 | + if not self.f.closed: |
699 | + self.f.close() |
700 | + |
701 | + def safe_write(self, data): |
702 | + if not self.f.closed: |
703 | + self.f.write(data) |
704 | + |
705 | + def _default_enter_func(self, o): |
706 | + return "Call " + o.short_call_string() |
707 | + |
708 | + def _default_exit_func(self, o): |
709 | + return "Return from {0} with value {1}".format(o["co_name"], |
710 | + o.short_return_value()) |
711 | + |
712 | + def _default_exception_func(self, o): |
713 | + return "Encountered exception" #@@@ more info TODO |
714 | + |
715 | + def _default_prefix_func(self, o): |
716 | + return ("[%s][%03d:%s]" + " "*self.depth) % (o["label"], |
717 | + o["f_lineno"], |
718 | + _utils.pretty_resize_string(o.nice_filename(), 15, exact=True)) |
719 | + |
720 | + def enter(self, o): |
721 | + self.depth += 1 |
722 | + self.safe_write(self.prefix_func(o) + self.enter_func(o) + "\n") |
723 | + return None |
724 | + |
725 | + def exit(self, o): |
726 | + self.safe_write(self.prefix_func(o) + self.exit_func(o) + "\n") |
727 | + self.depth -= 1 |
728 | + return None |
729 | + |
730 | + def exception(self, o): |
731 | + self.safe_write(self.prefix_func(o) + self.exception_func(o) + "\n") |
732 | + return None |
733 | + |
734 | + def logging(self, o, event_type_string): |
735 | + self.safe_write("ERROR TRACING with event: %s on line %d in %s\n" % ( |
736 | + event_type_string, o["f_lineno"], o["co_filename"])) |
737 | + return None |
738 | + |
739 | + |
740 | +class LogFileOutput(PlainTextOutput): |
741 | + """ |
742 | + Convienience function to send plaintext output to a file. |
743 | + """ |
744 | + def __init__(self, filename, verbosity, prefix_func=None, depth_spacer=" ", |
745 | + enter_func=None, exit_func=None, exception_func=None): |
746 | + self.f = open(filename, 'a', 1) #line-buffering |
747 | + super(LogFileOutput, self).__init__(verbosity, prefix_func=prefix_func, |
748 | + depth_spacer=depth_spacer, |
749 | + enter_func=enter_func, |
750 | + exit_func=exit_func, |
751 | + exception_func=exception_func) |
752 | + |
753 | + |
754 | +class ConsoleOutput(PlainTextOutput): |
755 | + """ |
756 | + Convienience function to send plaintext output to stdout. |
757 | + """ |
758 | + def __init__(self, filename, verbosity, prefix_func=None, depth_spacer=" ", |
759 | + enter_func=None, exit_func=None, exception_func=None): |
760 | + self.f = sys.stdout |
761 | + super(ConsoleOutput, self).__init__(verbosity, prefix_func=prefix_func, |
762 | + depth_spacer=depth_spacer, |
763 | + enter_func=enter_func, |
764 | + exit_func=exit_func, |
765 | + exception_func=exception_func) |
766 | + |
767 | + |
768 | +class DebugOutput(EntrailsOutput): |
769 | + """ |
770 | + Displays all output data with little formatting. |
771 | + """ |
772 | + def __init__(self): |
773 | + print "Entrails debug output started on {0}".format( |
774 | + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) |
775 | + |
776 | + def close(self): |
777 | + print "Entrails debug output ended on {0}".format( |
778 | + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) |
779 | + |
780 | + def enter(self, o): |
781 | + print "FUNCTION CALL: " + o.call_string() |
782 | + for k, v in o.iteritems(): |
783 | + print "\t{0}:{1}".format(k, str(v)) |
784 | + return None |
785 | + |
786 | + def exit(self, o): |
787 | + print "FUNCTION EXIT" |
788 | + for k, v in o.iteritems(): |
789 | + print "\t{0}:{1}".format(k, str(v)) |
790 | + return None |
791 | + |
792 | + def exception(self, o): |
793 | + print "EXCEPTION on line {0} in {1}".format(o["f_lineno"], |
794 | + o["co_filename"]) |
795 | + for k, v in o.iteritems(): |
796 | + print k + ": " + str(v) |
797 | + return None |
798 | + |
799 | + def logging(self, o, event_type_string): |
800 | + print "ERROR TRACING with event: {0} on line {1} in {2}".format(event_type_string, |
801 | + o["f_lineno"], |
802 | + o["co_filename"]) |
803 | + return None |
804 | + |
805 | + |
806 | +class HTMLOutput(EntrailsOutput): |
807 | + """ |
808 | + Not fully implemented. Basic HTML output. |
809 | + """ |
810 | + def __init__(self, filename, verbosity): |
811 | + self.depth = 0 |
812 | + self.f = open(filename, 'a', 1) #line-buffering |
813 | + self.f.write("<html><body><h1>Trace of program started on {0}</h1>\n".format(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) |
814 | + |
815 | + def __del__(self): |
816 | + self.f.write("\n</body></html>\n") |
817 | + self.f.close() |
818 | + |
819 | + def enter(self, codeframe): |
820 | + self.depth += 1 |
821 | + self.f.write(" > "*self.depth + "<b>Now entering:</b> " + codeframe["name"] + "<br />\n") |
822 | + return None |
823 | + |
824 | + def exit(self, codeframe): |
825 | + self.depth -= 1 |
826 | + self.f.write(" > "*(self.depth+1) + "<b>and exiting:</b> " + codeframe["name"] + " with return value " + str(codeframe["return_value"]) + "<br />\n") |
827 | + return None |
828 | + |
829 | + def exception(self, o): |
830 | + self.f.write("<h3>Encountered exception!</h3>") |
831 | + return None |
832 | + |
833 | + def logging(self, o, event_type_string): |
834 | + self.f.write("<h4>ERROR TRACING with event: %s on line %d in %s</h4>" % (event_type_string, o["f_lineno"], o["co_filename"])) |
835 | + return None |
836 | + |
837 | + |
838 | +class XMLOutput(EntrailsOutput): |
839 | + def __init__(self, filename): |
840 | + self.filename = filename |
841 | + self.root = ET.Element("data") |
842 | + |
843 | + def close(self): |
844 | + tree = ET.ElementTree(self.root) |
845 | + tree.write(self.filename, xml_declaration=True, method="xml") |
846 | + |
847 | + def write_out(self, codeframe, event_type): |
848 | + event = ET.SubElement(self.root, "event") |
849 | + event.attrib = {"timestamp" : "{0:f}".format(codeframe["timestamp"])} |
850 | + for k, v in codeframe.iteritems(): |
851 | + if k != "timestamp": |
852 | + ET.SubElement(event, k).text = str(v) |
853 | + ET.SubElement(event, "type").text = event_type |
854 | + ET.SubElement(event, "call_string").text = codeframe.call_string() |
855 | + ET.SubElement(event, "short_call_string").text = codeframe.short_call_string() |
856 | + ET.SubElement(event, "short_return_value").text = codeframe.short_return_value() |
857 | + ET.SubElement(event, "nice_filename").text = codeframe.nice_filename() |
858 | + |
859 | + def enter(self, codeframe): |
860 | + self.write_out(codeframe, "enter") |
861 | + |
862 | + def exit(self, codeframe): |
863 | + self.write_out(codeframe, "exit") |
864 | + |
865 | + def exception(self, codeframe): |
866 | + self.write_out(codeframe, "exception") |
867 | + |
868 | |
869 | === added file 'src/entrails/_trace.py' |
870 | --- src/entrails/_trace.py 1970-01-01 00:00:00 +0000 |
871 | +++ src/entrails/_trace.py 2014-07-07 09:01:02 +0000 |
872 | @@ -0,0 +1,258 @@ |
873 | +# ----------------------------------------------------------------------------- |
874 | +# _trace.py - Execution tracer |
875 | +# |
876 | +# 2012, Ben Eills |
877 | +# |
878 | +# Copyright 2012, 2014, Ensoft Ltd. |
879 | +# ----------------------------------------------------------------------------- |
880 | + |
881 | +"""Execution tracer.""" |
882 | + |
883 | +__all__ = ( |
884 | + "Entrails", |
885 | +) |
886 | + |
887 | + |
888 | +import collections |
889 | +import os.path |
890 | +import sys |
891 | +import time |
892 | + |
893 | +from . import _filter |
894 | +from . import _utils |
895 | + |
896 | + |
897 | +class _Frame(dict): |
898 | + """ |
899 | + Holds information about a frame. |
900 | + |
901 | + Use as a dict to access the important bits of the frame and arg |
902 | + parameters, along with timestamp and label. Also has convienience |
903 | + methods to generate function call string and class info. This is |
904 | + supplied to an output adapter's call(), return() and exception() |
905 | + methods. |
906 | + """ |
907 | + def parameters(self): |
908 | + """ |
909 | + Returns dict of form {parameter_name: argument_value}. |
910 | + """ |
911 | + # We rely on CPython putting arguments at start of f_locals (in reverse |
912 | + # order for some reason...). This is undocumented, but consistent. |
913 | + parameters = [(k, v) for k, v in self["f_locals"].iteritems() |
914 | + if k in self["co_varnames"][0:self["co_argcount"]]] |
915 | + parameters.reverse() |
916 | + return collections.OrderedDict(parameters) |
917 | + |
918 | + def arguments(self): |
919 | + """ |
920 | + Returns argument list, possibly excluding the first 'self' arg |
921 | + if the function is a method. |
922 | + |
923 | + Only designed for 'call' event (i.e. function entry) |
924 | + """ |
925 | + # Possibly chop off first self argument |
926 | + ignorer = 0 if not self.is_method() else 1 |
927 | + return self.parameters().values()[ignorer:] |
928 | + |
929 | + def string_arguments(self): |
930 | + """ |
931 | + Returns list of nice string argument representations. |
932 | + |
933 | + If the argument object encounters an error during repr() call |
934 | + (some standard library classes can do this under certain conditions) |
935 | + fail cleanly. |
936 | + """ |
937 | + strings = [] |
938 | + for arg in self.arguments(): |
939 | + try: |
940 | + strings.append(repr(arg)) |
941 | + except: |
942 | + strings.append('<repr error>') |
943 | + return strings |
944 | + |
945 | + def call_string(self): |
946 | + """ |
947 | + Returns a pretty string representation of the function call built |
948 | + from function name and args, nicely formatted/abbrieviated. |
949 | + |
950 | + Only designed for 'call' event (i.e. function entry) |
951 | + """ |
952 | + if self.is_method(): |
953 | + return '{0}.{1}({2})'.format(self.instance_name(), self['co_name'], |
954 | + ', '.join(self.string_arguments())) |
955 | + else: |
956 | + return '{0}({1})'.format(self['co_name'], |
957 | + ', '.join(self.string_arguments())) |
958 | + |
959 | + def short_call_string(self, length=40): |
960 | + """ |
961 | + Returns a pretty string of the function call, shortened to at |
962 | + most length characters. |
963 | + |
964 | + Only designed for 'call' event (i.e. function entry) |
965 | + """ |
966 | + return _utils.pretty_resize_string(self.call_string(), length) |
967 | + |
968 | + def short_return_value(self, length=40): |
969 | + """ |
970 | + Returns a pretty string representation of the return value, |
971 | + shortened to at most length characters. |
972 | + |
973 | + Only designed for 'call' event (i.e. function entry) |
974 | + """ |
975 | + try: |
976 | + return _utils.pretty_resize_string(repr(self['return_value']), length) |
977 | + except: |
978 | + return _utils.pretty_resize_string('<repr error>', length) |
979 | + |
980 | + # Do we close quotation marks in string return values? |
981 | + # if len(repr(rv)) > 40: |
982 | + # if rv.__class__.__name__ == 'str': |
983 | + # return repr(rv)[0:length-4]+"...'" |
984 | + # else: |
985 | + # return repr(rv)[0:length-3]+"..." |
986 | + # else: |
987 | + # return repr(rv) |
988 | + |
989 | + def nice_filename(self): |
990 | + """ |
991 | + Nice filename, without directory segment. |
992 | + """ |
993 | + return os.path.basename(self["co_filename"]) |
994 | + |
995 | + def is_method(self): |
996 | + """ |
997 | + True if function is an instance method (and the programmer |
998 | + did not defy all convention by abandonning the use of 'self' |
999 | + as the instance pointer). |
1000 | + Only designed for 'call' event (i.e. function entry) |
1001 | + |
1002 | + TODO: What happens for class methods/other decorated methods? |
1003 | + """ |
1004 | + if self['co_varnames'] and self['co_varnames'][0] == 'self': |
1005 | + return True |
1006 | + return False |
1007 | + |
1008 | + def instance_name(self): |
1009 | + """ |
1010 | + Class instance name of method, or empty string if not method. |
1011 | + Only designed for 'call' event (i.e. function entry) |
1012 | + We cache value after first proper lookup. |
1013 | + """ |
1014 | + if 'instance_name' not in self: |
1015 | + self['instance_name'] = self._retrieve_instance_name() |
1016 | + return self['instance_name'] |
1017 | + |
1018 | + def _retrieve_instance_name(self): |
1019 | + """ |
1020 | + Get class instance name by performance-intensive lookup. |
1021 | + Should only be called on methods. |
1022 | + """ |
1023 | + try: |
1024 | + # This is confusing. We go into the previous (parent) frame and |
1025 | + # find the local variable name in that pointing to a our class. |
1026 | + for k, v in self['f_back'].f_locals.items(): |
1027 | + if v == self['f_locals']['self']: |
1028 | + return k |
1029 | + except: |
1030 | + return '<lookup error>' |
1031 | + |
1032 | + def class_name(self): |
1033 | + """ |
1034 | + Class name of method, or empty string if not method. |
1035 | + Only designed for 'call' event (i.e. function entry) |
1036 | + Should only be called on methods. |
1037 | + """ |
1038 | + try: |
1039 | + return self["f_locals"]["self"].__class__.__name__ |
1040 | + except: |
1041 | + return '<lookup error>' |
1042 | + |
1043 | + |
1044 | +class Entrails(object): |
1045 | + """ |
1046 | + Implements entrails functionality to monitor program execution flow. |
1047 | + |
1048 | + Instantiate at in process to be traced, add outputs and call start_trace(). |
1049 | + Run end_trace() to safely close outputs and stop tracing. |
1050 | + """ |
1051 | + event_names = { 'call' : 'enter', |
1052 | + 'return' : 'exit', |
1053 | + 'exception' : 'exception' } |
1054 | + |
1055 | + def __init__(self, |
1056 | + label='<untitled>', |
1057 | + filters=(_filter.LibraryFilter(), _filter.SelfFilter())): |
1058 | + self._outputs = [] |
1059 | + self._init_time = time.time() |
1060 | + self._label = label |
1061 | + self._filters = filters |
1062 | + |
1063 | + def filter(self, obj): |
1064 | + """ |
1065 | + Returns True if object remains unfiltered, False otherise. |
1066 | + """ |
1067 | + if all(f.filter(obj) for f in self._filters): |
1068 | + return True |
1069 | + return False |
1070 | + |
1071 | + def _trace_function(self, trace, event, arg): |
1072 | + # Build Frame |
1073 | + obj = _Frame() |
1074 | + obj["label"] = self._label |
1075 | + obj["timestamp"] = time.time() - self._init_time |
1076 | + obj["f_lineno"] = trace.f_lineno |
1077 | + obj["f_back"] = trace.f_back |
1078 | + obj["f_locals"] = trace.f_locals |
1079 | + obj["co_name"] = trace.f_code.co_name |
1080 | + obj["co_argcount"] = trace.f_code.co_argcount |
1081 | + obj["co_filename"] = trace.f_code.co_filename |
1082 | + obj["co_names"] = trace.f_code.co_names |
1083 | + obj["co_varnames"] = trace.f_code.co_varnames |
1084 | + |
1085 | + # Filter |
1086 | + # If this object is to be discarded, return trace_function to |
1087 | + # indicate that we wish to continue tracing. |
1088 | + if not self.filter(obj): |
1089 | + return self._trace_function |
1090 | + |
1091 | + # Possibly add return_value. If event is non-return, arg isn't really |
1092 | + # that useful. |
1093 | + if event is 'return': |
1094 | + obj["return_value"] = arg |
1095 | + |
1096 | + if event in self.event_names: |
1097 | + for output in self._outputs: |
1098 | + #try: |
1099 | + getattr(output, self.event_names[event])(obj) |
1100 | + return self._trace_function |
1101 | + |
1102 | + def add_output(self, output_object): |
1103 | + """ |
1104 | + Adds output adapter, accepting argument of type EntrailsOutput. |
1105 | + """ |
1106 | + self._outputs.append(output_object) |
1107 | + |
1108 | + def remove_output(self, output_object): |
1109 | + """ |
1110 | + Cease sending tracing data to a particular output. |
1111 | + Removes from internal list and runs output.__del__()@@@ |
1112 | + """ |
1113 | + output_object.close() |
1114 | + if output_object in self._outputs: |
1115 | + self._outputs.remove(output_object) |
1116 | + |
1117 | + def start_trace(self): |
1118 | + """ |
1119 | + Start tracing. |
1120 | + """ |
1121 | + sys.settrace(self._trace_function) |
1122 | + |
1123 | + def end_trace(self): |
1124 | + """ |
1125 | + Stop tracing. Safely removes all outputs. |
1126 | + """ |
1127 | + for o in self._outputs: |
1128 | + self.remove_output(o) |
1129 | + sys.settrace(None) |
1130 | + |
1131 | |
1132 | === added file 'src/entrails/_utils.py' |
1133 | --- src/entrails/_utils.py 1970-01-01 00:00:00 +0000 |
1134 | +++ src/entrails/_utils.py 2014-07-07 09:01:02 +0000 |
1135 | @@ -0,0 +1,29 @@ |
1136 | +# ----------------------------------------------------------------------------- |
1137 | +# _utils.py - Shared utilities |
1138 | +# |
1139 | +# July 2014, Phil Connell |
1140 | +# |
1141 | +# Copyright 2014, Ensoft Ltd. |
1142 | +# ----------------------------------------------------------------------------- |
1143 | + |
1144 | +"""Shared utilities.""" |
1145 | + |
1146 | +__all__ = ( |
1147 | + "pretty_resize_string", |
1148 | +) |
1149 | + |
1150 | + |
1151 | +def pretty_resize_string(string, length=25, cutoff='back', exact=False): |
1152 | + """ |
1153 | + Shortens or lengthens a string to be at most length characters long. |
1154 | + |
1155 | + cutoff should be 'front' or 'back', dictating where the ellipsis goes. |
1156 | + exact toggles whitespace padding. |
1157 | + """ |
1158 | + if len(string) > length: |
1159 | + cutoff_string = '{0}...' if cutoff is 'back' else '...{0}' |
1160 | + return cutoff_string.format(string[3-length:]) |
1161 | + else: |
1162 | + exact_string = '{:>25}' if exact else '{0}' |
1163 | + return exact_string.format(string) |
1164 | + |
1165 | |
1166 | === modified file 'src/trace.py' |
1167 | --- src/trace.py 2012-08-20 10:31:04 +0000 |
1168 | +++ src/trace.py 2014-07-07 09:01:02 +0000 |
1169 | @@ -1,4 +1,4 @@ |
1170 | -#!/usr/bin/env python2.7 |
1171 | +#!/usr/bin/env python |
1172 | # ----------------------------------------------------------------------------- |
1173 | # EnTrails - Python Execution Tracing Module |
1174 | # Copyright 2012, Ensoft Ltd. |
1175 | @@ -106,13 +106,13 @@ |
1176 | label = args.label |
1177 | |
1178 | if args.output == 'debug': |
1179 | - output_code_string = "_entrails_o = entrails.OutputDebug()" |
1180 | + output_code_string = "_entrails_o = entrails.DebugOutput()" |
1181 | elif args.output == 'logfile': |
1182 | - output_code_string = "_entrails_o = entrails.OutputLogFile(\"%s\", 1)" % args.filename |
1183 | + output_code_string = "_entrails_o = entrails.LogFileOutput(\"%s\", 1)" % args.filename |
1184 | elif args.output == 'console': |
1185 | - output_code_string = "_entrails_o = entrails.OutputConsole(1)" |
1186 | + output_code_string = "_entrails_o = entrails.ConsoleOutput(1)" |
1187 | elif args.output == 'xml': |
1188 | - output_code_string = "_entrails_o = entrails.OutputXML(\"%s\")" % args.filename |
1189 | + output_code_string = "_entrails_o = entrails.XMLOutput(\"%s\")" % args.filename |
1190 | |
1191 | if args.action == "add": |
1192 | if not already_modified(args.sourcefile): |
Will sort out the namespacing (stick everything in the top level), and address the couple of cosmetic comments.