Home | History | Annotate | Download | only in cr
      1 # Copyright 2013 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Module scan and load system.
      6 
      7 The main interface to this module is the Scan function, which triggers a
      8 recursive scan of all packages and modules below cr, with modules being
      9 imported as they are found.
     10 This allows all the plugins in the system to self register.
     11 The aim is to make writing plugins as simple as possible, minimizing the
     12 boilerplate so the actual functionality is clearer.
     13 """
     14 from importlib import import_module
     15 import os
     16 import sys
     17 
     18 import cr
     19 
     20 # This is the name of the variable inserted into modules to track which
     21 # scanners have been applied.
     22 _MODULE_SCANNED_TAG = '_CR_MODULE_SCANNED'
     23 
     24 
     25 class AutoExport(object):
     26   """A marker for classes that should be promoted up into the cr namespace."""
     27 
     28 
     29 def _AutoExportScanner(module):
     30   """Scan the modules for things that need wiring up automatically."""
     31   for name, value in module.__dict__.items():
     32     if isinstance(value, type) and issubclass(value, AutoExport):
     33       # Add this straight to the cr module.
     34       if not hasattr(cr, name):
     35         setattr(cr, name, value)
     36 
     37 
     38 scan_hooks = [_AutoExportScanner]
     39 
     40 
     41 def _Import(name):
     42   """Import a module or package if it is not already imported."""
     43   module = sys.modules.get(name, None)
     44   if module is not None:
     45     return module
     46   return import_module(name, None)
     47 
     48 
     49 def _ScanModule(module):
     50   """Runs all the scan_hooks for a module."""
     51   scanner_tags = getattr(module, _MODULE_SCANNED_TAG, None)
     52   if scanner_tags is None:
     53     # First scan, add the scanned marker set.
     54     scanner_tags = set()
     55     setattr(module, _MODULE_SCANNED_TAG, scanner_tags)
     56   for scan in scan_hooks:
     57     if scan not in scanner_tags:
     58       scanner_tags.add(scan)
     59       scan(module)
     60 
     61 
     62 def _ScanPackage(package):
     63   """Scan a package for child packages and modules."""
     64   modules = []
     65   # Recurse sub folders.
     66   for path in package.__path__:
     67     try:
     68       basenames = os.listdir(path)
     69     except OSError:
     70       basenames = []
     71     for basename in basenames:
     72       fullpath = os.path.join(path, basename)
     73       if os.path.isdir(fullpath):
     74         name = '.'.join([package.__name__, basename])
     75         child = _Import(name)
     76         modules.extend(_ScanPackage(child))
     77       elif basename.endswith('.py') and not basename.startswith('_'):
     78         name = '.'.join([package.__name__, basename[:-3]])
     79         modules.append(name)
     80   return modules
     81 
     82 
     83 def Scan():
     84   """Scans from the cr package down, loading modules as needed.
     85 
     86   This finds all packages and modules below the cr package, by scanning the
     87   file system. It imports all the packages, and then runs post import hooks on
     88   each module to do any automated work. One example of this is the hook that
     89   finds all classes that extend AutoExport and copies them up into the cr
     90   namespace directly.
     91 
     92   Modules are allowed to refer to each other, their import will be retried
     93   until it succeeds or no progress can be made on any module.
     94   """
     95   remains = _ScanPackage(cr)
     96   progress = True
     97   modules = []
     98   while progress and remains:
     99     progress = False
    100     todo = remains
    101     remains = []
    102     for name in todo:
    103       try:
    104         module = _Import(name)
    105         modules.append(module)
    106         _ScanModule(module)
    107         progress = True
    108       except:  # sink all errors here pylint: disable=bare-except
    109         # Try this one again, if progress was made on a possible dependency
    110         remains.append(name)
    111   if remains:
    112     # There are modules that won't import in any order.
    113     # Print all the errors as we can't determine root cause.
    114     for name in remains:
    115       try:
    116         _Import(name)
    117       except ImportError as e:
    118         print 'Failed importing', name, ':', e
    119     exit(1)
    120   # Now scan all the found modules one more time.
    121   # This happens after all imports, in case any imports register scan hooks.
    122   for module in modules:
    123     _ScanModule(module)
    124