about_text = '''Blinkenlights version 0.1 Blinkenlights shows you activity in Python code. The window displays one monitor light per module currently loaded in Python. Each light flashes when activity is detected in the corresponding module. Click a light to see more detail. Copyright (c) 2004 Shane Hathaway Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ACHTUNG! ALLES LOOKENSPEEPERS! Das computermachine ist nicht fuer gefingerpoken und mittengrabben. Ist easy schnappen der springenwerk, blowenfusen und poppencorken mit spitzensparken. Ist nicht fuer gewerken bei das dumpkopfen. Das rubbernecken sichtseeren keepen das cotten-pickenen hans in das pockets muss; relaxen und watchen das blinkenlichten. http://www.catb.org/~esr/jargon/html/B/blinkenlights.html http://hathawaymix.org/Software/Sketches ''' import os import sys import wx if wx.VERSION < (2,5,2): sys.stderr.write( "This software requires wxPython version 2.5.2 or later.\n") sys.exit(1) # script_thread is the thread where the script runs script_thread = None class Prefs: light_width = 6 light_height = 12 margin = 4 spacing = 2 # Space between text and lights interval = 30 # milliseconds clear_frames = True class ModuleMonitor: """Holds information about a loaded Python module""" position = None # (x, y) value = 0 def __init__(self, module, name): self.module_name = name self.filename = getattr(module, '__file__', None) if self.filename: # Figure out what part of sys.path this module was loaded from. parts = name.split('.') basepath = self.filename dir, fn = os.path.split(basepath) if os.path.splitext(fn)[0] == '__init__': # A package basepath = dir for part in parts: basepath, n = os.path.split(basepath) self.basepath = os.path.abspath(basepath) else: self.basepath = None class Floater(wx.Frame): """A floating indicator of what the user is pointing at.""" def __init__(self, parent): wx.Frame.__init__( self, parent, -1, "module info", style=wx.FRAME_NO_TASKBAR | wx.FRAME_TOOL_WINDOW) self.L1 = wx.StaticText(self, -1, "") self.L2 = wx.StaticText(self, -1, "") def display(self, line1, line2, position): self.L1.SetLabel(line1) self.L2.SetLabel(line2) s1 = self.L1.GetBestSize() self.L1.SetSize(s1) s2 = self.L2.GetBestSize() self.L2.SetSize(s2) self.L1.SetPosition((2, 2)) self.L2.SetPosition((2, s1.height + 4)) self.SetSize((max(s1.width, s2.width) + 4, s1.height + s2.height + 6)) self.SetPosition(position) self.Show() class LightGrid(wx.ScrolledWindow): def __init__(self, parent, floater): wx.ScrolledWindow.__init__(self, parent, -1) self.floater = floater self.SetBackgroundColour(wx.BLACK) self.mms = {} #{module_name: module_monitor} self.paths = [] # [(basepath, [module_monitor])] self.regions = [] # [(y, [module_monitor])] self.lit = {} # {module_name: module_monitor} self.find_modules() self.new_modules = False self.resized = True self.module_panel = None self.buffer = None self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_MOTION, self.OnMouseMotion) self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) self.Bind(wx.EVT_TIMER, self.OnTimer) self.timer = wx.Timer(self) self.timer.Start(Prefs.interval, wx.TIMER_CONTINUOUS) def set_module_panel(self, m): self.module_panel = m def find_modules(self): """Locate modules and generate self.mms and self.paths. """ d = {} # {basepath: {module: module_monitor}} for module in sys.modules.values(): name = getattr(module, '__name__', None) if not name: continue mm = self.mms.get(name) if mm is None: mm = ModuleMonitor(module, name) self.mms[name] = mm if mm.basepath is not None: d.setdefault(mm.basepath, {})[name] = mm # else the module is perhaps an extension. # Sort the base paths by the order of sys.path. ordered = [] for p in sys.path: if d.has_key(p): ordered.append((p, d[p])) del d[p] if d: # Couldn't match some of the modules with sys.path. d = d.items() d.sort() ordered.extend(d) # Prepare self.paths paths = [] for basepath, modules in ordered: items = modules.items() items.sort() paths.append((basepath, [mm for (name, mm) in items])) self.paths = paths def draw_surface(self): """Position and draw all path labels and module lights. """ size = self.GetClientSize() margin = Prefs.margin light_height = Prefs.light_height light_width = Prefs.light_width box_width = size.width - margin y = margin labels = [] # [(label, y)] self.regions = [] for basepath, mms in self.paths: text_width, text_height = self.GetTextExtent(basepath) labels.append((basepath, y)) y += text_height + Prefs.spacing x = margin region_mms = [] for mm in mms: if x + light_width >= box_width: self.regions.append((y, region_mms)) region_mms = [] x = margin y += light_height region_mms.append(mm) mm.position = (x, y) x += light_width if region_mms: self.regions.append((y, region_mms)) y += light_height + Prefs.spacing y += margin self.SetScrollbars(1, 1, size.width, y) self.buffer = wx.EmptyBitmap(size.width, y) dc = wx.BufferedDC(None, self.buffer) dc.BeginDrawing() dc.SetBackground(wx.BLACK_BRUSH) dc.Clear() dc.SetTextForeground(wx.Colour(192, 192, 192)) for text, y in labels: dc.DrawText(text, margin, y) self.outline_pen = wx.Pen(wx.Colour(63, 63, 63)) self.light_size = (light_width, light_height) for mm in self.mms.values(): self.draw_light(dc, mm) dc.EndDrawing() def draw_light(self, dc, mm): """Draw one light. """ if not mm.position: return dc.SetPen(self.outline_pen) if mm.value: dc.SetBrush(wx.RED_BRUSH) else: dc.SetBrush(wx.BLACK_BRUSH) dc.DrawRectangle(*(mm.position + self.light_size)) def identify_monitor(self, x, y): """Returns the ModuleMonitor at (x, y) or None. """ light_height = Prefs.light_height for ry, region in self.regions: if ry > y: break if y >= ry and y < ry + light_height: # The mouse is over this row light_no = (x - Prefs.margin) / Prefs.light_width if light_no < len(region): return region[light_no] return None def OnPaint(self, event): if self.new_modules: self.new_modules = False self.find_modules() self.resized = True if self.resized or self.buffer is None: self.resized = False self.draw_surface() # Blit the bitmap. wx.BufferedPaintDC(self, self.buffer) def OnSize(self, event): self.resized = True self.Refresh() def OnMouseMotion(self, event): x0, y0 = self.GetViewStart() mm = self.identify_monitor(x0 + event.m_x, y0 + event.m_y) if mm is None: self.floater.Hide() else: # Display module info at an absolute position. pos = self.ClientToScreen(event.GetPosition()) p = (pos.x + 8, pos.y + 8) self.floater.display(mm.module_name, mm.filename, p) def OnMouseDown(self, event): mp = self.module_panel if mp is None: return x0, y0 = self.GetViewStart() mm = self.identify_monitor(x0 + event.m_x, y0 + event.m_y) mp.focus(mm) def OnLeave(self, event): self.floater.Hide() def OnTimer(self, event): if not script_thread.isAlive(): sys.exit(0) if self.buffer is None: return dc = wx.BufferedDC(None, self.buffer) dc.BeginDrawing() changed = False sample = past_frames.copy() if Prefs.clear_frames: past_frames.clear() # Note it's possible to drop a frame or two. c = current_frame if c: name, line, func = c if name: sample.setdefault(name, {})[line] = func if self.module_panel is not None: self.module_panel.got_sample(sample) lit = self.lit for name, mm in lit.items(): if not sample.has_key(name): # Turn off a light changed = True mm.value = 0 del lit[name] self.draw_light(dc, mm) for name in sample.keys(): if not lit.has_key(name): # Turn on a light changed = True mm = self.mms.get(name) if mm is None: # A new module has been loaded. self.new_modules = True module = sys.modules.get(name) if module is None: # This can happen if sys.modules is a little confused. continue mm = ModuleMonitor(module, name) self.mms[name] = mm mm.value = 1 lit[name] = mm self.draw_light(dc, mm) dc.EndDrawing() if changed: self.Refresh(False) class ModulePanel(wx.Panel): def __init__(self, parent, id=-1): wx.Panel.__init__(self, parent, id) # controls sizer = wx.BoxSizer(wx.VERTICAL) self.text = wx.TextCtrl( self, -1, style=wx.TE_MULTILINE | wx.TE_READONLY) self.show_about_text() sizer.Add(self.text, 1, wx.EXPAND) self.SetAutoLayout(1) self.SetSizer(sizer) self.Layout() self.mm = None # A ModuleMonitor or None self.hit_lines = {} # line_no -> func_name def show_about_text(self): paragraphs = [''] for line in about_text.split('\n'): if not line: paragraphs.append('') else: paragraphs[-1] += line.strip() + ' ' self.text.SetValue('\n\n'.join(paragraphs)) def focus(self, mm): self.mm = mm if mm is None: self.show_about_text() else: self.hit_lines = {} self.got_sample({}) def got_sample(self, sample): if self.mm is None: return s = sample.get(self.mm.module_name) if s is not None: self.hit_lines.update(s) out = [ 'Selected module: %s' % self.mm.module_name, 'File: %s' % self.mm.filename, 'Active lines:', ] items = self.hit_lines.items() items.sort() out.extend([' %s (%s)' % (line, name) for line, name in items]) out = '\n'.join(out) if self.text.GetValue() != out: self.text.SetValue(out) class Frame(wx.Frame): """Frame class.""" def __init__(self, parent=None, id=-1, title='Blinkenlights', pos=wx.DefaultPosition, size=(300, 400)): """Create a Frame instance.""" wx.Frame.__init__(self, parent, id, title, pos, size) self.SetPosition((0, 0)) self.SetSize((300, 700)) floater = Floater(self) splitter = wx.SplitterWindow(self, -1) self.grid = LightGrid(splitter, floater) self.module_panel = ModulePanel(splitter, -1) self.grid.set_module_panel(self.module_panel) splitter.SetMinimumPaneSize(20) splitter.SplitHorizontally(self.grid, self.module_panel) class App(wx.App): """Application class.""" def OnInit(self): self.frame = Frame() self.frame.Show() self.SetTopWindow(self.frame) return True current_frame = None # (module, line, func_name) past_frames = {} # {module -> {line: func_name}} def profile_func(frame, event, arg): """Adds the current execution pointer to current_frame and past_frames. """ global current_frame, past_frames name = frame.f_globals.get('__name__') line = frame.f_lineno func = frame.f_code.co_name current_frame = (name, line, func) if not name: return lines = past_frames.get(name) if lines is None: past_frames[name] = lines = {} lines[line] = func real_start_new_thread = None def patched_start_new_thread(function, args, kwargs=None): # Adds profiling to every new thread def start(*args, **kwargs): sys.settrace(profile_func) function(*args, **kwargs) if kwargs is not None: real_start_new_thread(start, args, kwargs) else: real_start_new_thread(start, args) def patch_thread_module(): global real_start_new_thread import thread import threading real_start_new_thread = thread.start_new_thread thread.start_new_thread = patched_start_new_thread threading._start_new_thread = patched_start_new_thread def patched_signal(signalnum, handler): # TODO: redirect signal() calls to the main thread via the wx event loop. pass def patch_signal_module(): try: import signal except ImportError: return signal.signal = patched_signal def run_script(): del sys.argv[0] # Remove blinkenlights.py script = sys.argv[0] sys.path.insert(0, os.path.abspath(os.path.dirname(script))) execfile(script, { '__name__': '__main__', '__builtins__': __builtins__, '__file__': script, }) def main(): global script_thread patch_signal_module() patch_thread_module() app = App() if len(sys.argv) > 1: # Run a Python script in another thread target = run_script else: # Interactive mode import code target = code.interact # Use the real console, since the wxPython console window # does not inject to stdin. sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ sys.stdin = sys.__stdin__ import threading script_thread = threading.Thread(target=target) script_thread.setDaemon(True) # Exit when the window closes script_thread.start() app.MainLoop() if __name__ == '__main__': main()