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 7The main interface to this module is the Scan function, which triggers a 8recursive scan of all packages and modules below cr, with modules being 9imported as they are found. 10This allows all the plugins in the system to self register. 11The aim is to make writing plugins as simple as possible, minimizing the 12boilerplate so the actual functionality is clearer. 13""" 14from importlib import import_module 15import os 16import sys 17 18import 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 25class AutoExport(object): 26 """A marker for classes that should be promoted up into the cr namespace.""" 27 28 29def _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 38scan_hooks = [_AutoExportScanner] 39 40 41def _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 49def _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 62def _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 83def 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