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

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
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.HTMLOutput).

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

Will sort out the namespacing (stick everything in the top level), and address the couple of cosmetic comments.

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):

Subscribers

People subscribed via source and target branches