Lenard Lindstrom avatar Lenard Lindstrom committed 7d253bd

trackmod: Add module pattern and continuous recording

Comments (0)

Files changed (6)

 This package tracks module imports and can generate a report on program
 completion. It has been added to Pygame's SVN with the purpose of
 incorporating it into the unit tests. The package still needs testing.
-The package has been successfully tried on a zipped package. Still
-should have a way to limit what modules will be tracked.
+The package has been successfully tried on a zipped package.
 
 
 See the playmus.py example for how it is currently used.
 
-rev 1740: - Report generation now optional.
-          - Added a report end feature.
+Log:
 
-rev 1741: - Added a data inspection api.
+rev 1743: - Added optional module pattern to limit which modules are monitored.
+          - Added optional continuous attribute access recording.
+
+rev 1741: - Added a data inspection API.
           - Now have a data collection stop function.
           - For testing, reload resets the entire package.
           - Readied for capture of all module attribute accesses.
 
+rev 1740: - Report generation now optional.
+          - Added a record end feature.
 import sys
 
 import trackmod
-trackmod.begin('playmus_rep.txt')
+trackmod.begin('playmus_rep.txt',
+               ['pygame', 'pygame.mixer', 'pygame.mixer_music'], True)
 import pygame
 
 MusicEnd = pygame.USEREVENT

trackmod/__init__.py

 import sys
 import atexit
 
-from trackmod import importer
+from trackmod import importer, module
 
 try:
     installed
     installed = False
 else:
     # reloaded; reload submodules.
-    reload(importer)  # implicit reporter reload.
+    reload(importer)  # implicit reporter and module reload.
 
 def print_(*args, **kwds):
     stream = kwds.get('file', sys.stdout)
         else:
             _write_report(repfile)
 
-def begin(repfile=None):
+def begin(repfile=None, pattern=None, continuous=False):
     """Start collecting import and module access information
 
-    repfile, if provided, is the destination for an end-of-run module import
-    and access report. It can be either a file path or an open file object.
+    repfile (default no file) is the destination for an
+    end-of-run module import and access report. It can be either a file
+    path or an open file object.
+
+    pattern (default ['*']) is a list of modules on which to collect data. It
+    is a list of one or more dotted full module names. An asterisk '*' is a
+    wild card an matches everything. Examples:
+      ['pygame']               Will on report on top level pygame package
+      ['pygame', 'numpy']      Only top level pygame and numpy modules
+      ['pygame', 'pygame.surface']
+                               pygame and pygame.surface
+      ['pygame', 'pygame.*']   pygame and all its submodules
+      ['*']                    everything
+
+    continous (default False) indicates whether per-module attribute access
+    recording should stop with the first access or be continuous. Set False
+    to stop after the first access, True for continuous recording.
 
     """
     global installed, collecting
             return
     except NameError:
         collecting = True
+    if continuous:
+        module.set_report_mode('continuous')
+    importer.begin(pattern)
 
 def end():
     global collecting
     collecting = False
     reporter.end()
     importer.end()
+    module.set_report_mode('quit')
 
 reporter.begin()  # Keep this last.
 

trackmod/importer.py

 """A sys.meta_path importer for tracking module usage."""
 
 import sys
-
-from trackmod import module, reporter
+from trackmod import module, reporter, namereg
 
 try:
     collect_data
 else:
     # reload: reload imported modules.
     reload(module)  # implicit reload of reporter
+    reload(namereg)
 
-collect_data = True
+no_modules = []  # Contains nothing.
+modules_of_interest = no_modules
+
 
 class Loader(object):
     def __init__(self, fullname, module):
         return self.module
 
 def find_module(fullname, path=None):
-    if collect_data and fullname not in sys.modules:
+    if fullname in modules_of_interest and fullname not in sys.modules:
         # Put this first so the order of inserts follows the order of calls to
         # find_module: package, subpackage, etc.
         reporter.add_import(fullname)
         return None
 
 def end():
-    global collect_data
-    collect_data = False
+    global modules_of_interest
+    modules_of_interest = no_modules
+
+def begin(pattern=None):
+    global modules_of_interest, collect_data
+    if pattern is None:
+        pattern = ['*']
+    modules_of_interest = namereg.NameRegistry(pattern)

trackmod/module.py

     reload(reporter)
 
 ModuleType = type(reporter)
+getattribute = ModuleType.__getattribute__
 
 
 class Module(ModuleType):
         if attr in ['__name__', '__path__']:
             # The name attribute is the one attribute guaranteed to not trigger
             # an import. __path__ is just noise in the reporting.
-            return ModuleType.__getattribute__(self, attr)
+            return getattribute(self, attr)
         report(self, attr)
-        # At this point self's type has changed so getattr will not
-        # recursively call this method.
-        return getattr(self, attr)
+        return getattribute(self, attr)
 
 
-def report(module, attr):
+def report_oneshot(module, attr):
     name = module.__name__  # Safe: no recursive call on __name__.
     lock = keylock.Lock(name)
     try:
         ModuleType.__setattr__(module, '__class__', Module)
     finally:
         lock.free()
+
+def report_continuous(module, attr):
+    name = module.__name__  # Safe: no recursive call on __name__.
+    lock = keylock.Lock(name)
+    try:
+        reporter.add_access(name, attr)
+    finally:
+        lock.free()
+
+def report_quit(module, attr):
+    name = module.__name__  # Safe: no recursive call on __name__.
+    lock = keylock.Lock(name)
+    try:
+        ModuleType.__setattr__(module, '__class__', Module)
+    finally:
+        lock.free()
+    
+def set_report_mode(mode=None):
+    """Set whether access checking is oneshot or continuous
+
+    if mode (default 'oneshot') is 'oneshot' or None then a TrackerModule
+    module will stop recording attribute accesses after the first non-trivial
+    access. If 'continuous' then all attribute accesses are recorded. If
+    'quit' then access recording stops and further calls to this function
+    have no effect.
+
+    """
+    global report
+    
+    if report is report_quit:
+        return
+    if mode is None:
+        mode = 'oneshot'
+    if mode == 'oneshot':
+        report = report_oneshot
+    elif mode == 'continuous':
+        report = report_continuous
+    elif mode == 'quit':
+        report = report_quit
+    else:
+        raise ValueError("Unknown mode %s" % mode)
+
+report = report_oneshot

trackmod/namereg.py

+# module trackmod.namereg
+
+class NameRegistry(object):
+    
+    class AllRegistered(object):
+        terminal = True
+        def register(self, names):
+            return
+        def __contains__(self, name):
+            return True
+    all_registered = AllRegistered()
+
+    class AllFound(object):
+        def __init__(self, value):
+            self.value = value
+        def __getitem__(self, key):
+            return self.value
+    all_found = AllFound(all_registered)
+
+    def __init__(self, names=None):
+        self.names = {}
+        if names is not None:
+            self.add(names)
+        self.terminal = False
+
+    def add(self, names):
+        if names is None:
+            self.terminal = True
+            return
+        for name in names:
+            parts = name.split('.', 1)
+            first = parts[0]
+            if first == '*':
+                self.names = self.all_found
+                return
+            else:
+                try:
+                    sub_registry = self.names[first]
+                except KeyError:
+                    sub_registry = NameRegistry()
+                    self.names[first] = sub_registry
+                if len(parts) == 2:
+                    sub_registry.add(parts[1:])
+                else:
+                    sub_registry.terminal = True
+
+    def __contains__(self, name):
+        parts = name.split('.', 1)
+        try:
+            sub_registry = self.names[parts[0]]
+        except KeyError:
+            return False
+        # This uses a conditional or.
+        if len(parts) == 1:
+            return sub_registry.terminal
+        return parts[1] in sub_registry
+
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.