OLD | NEW |
(Empty) | |
| 1 import copy |
| 2 import imp |
| 3 import inspect |
| 4 import os |
| 5 import sys |
| 6 |
| 7 from common import chromium_utils |
| 8 |
| 9 from .recipe_util import RECIPE_DIRS, MODULE_DIRS |
| 10 from .recipe_api import RecipeApi |
| 11 from .recipe_test_api import RecipeTestApi, DisabledTestData, ModuleTestData |
| 12 |
| 13 |
| 14 def load_recipe_modules(mod_dirs): |
| 15 def patchup_module(name, submod): |
| 16 submod.NAME = name |
| 17 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) |
| 18 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) |
| 19 |
| 20 if hasattr(submod, 'config'): |
| 21 for v in submod.config.__dict__.itervalues(): |
| 22 if hasattr(v, 'I_AM_A_CONFIG_CTX'): |
| 23 assert not submod.CONFIG_CTX, ( |
| 24 'More than one configuration context: %s' % (submod.config)) |
| 25 submod.CONFIG_CTX = v |
| 26 assert submod.CONFIG_CTX, 'Config file, but no config context?' |
| 27 |
| 28 submod.API = getattr(submod, 'API', None) |
| 29 for v in submod.api.__dict__.itervalues(): |
| 30 if inspect.isclass(v) and issubclass(v, RecipeApi): |
| 31 assert not submod.API, ( |
| 32 'More than one Api subclass: %s' % submod.api) |
| 33 submod.API = v |
| 34 assert submod.API, 'Submodule has no api? %s' % (submod) |
| 35 |
| 36 submod.TEST_API = getattr(submod, 'TEST_API', None) |
| 37 if hasattr(submod, 'test_api'): |
| 38 for v in submod.test_api.__dict__.itervalues(): |
| 39 if inspect.isclass(v) and issubclass(v, RecipeTestApi): |
| 40 assert not submod.TEST_API, ( |
| 41 'More than one TestApi subclass: %s' % submod.api) |
| 42 submod.TEST_API = v |
| 43 assert submod.API, ( |
| 44 'Submodule has test_api.py but no TestApi subclass? %s' |
| 45 % (submod) |
| 46 ) |
| 47 |
| 48 RM = 'RECIPE_MODULES' |
| 49 def find_and_load(fullname, modname, path): |
| 50 if fullname not in sys.modules or fullname == RM: |
| 51 try: |
| 52 fil, pathname, descr = imp.find_module(modname, |
| 53 [os.path.dirname(path)]) |
| 54 imp.load_module(fullname, fil, pathname, descr) |
| 55 finally: |
| 56 if fil: |
| 57 fil.close() |
| 58 return sys.modules[fullname] |
| 59 |
| 60 def recursive_import(path, prefix=None, skip_fn=lambda name: False): |
| 61 modname = os.path.splitext(os.path.basename(path))[0] |
| 62 if prefix: |
| 63 fullname = '%s.%s' % (prefix, modname) |
| 64 else: |
| 65 fullname = RM |
| 66 m = find_and_load(fullname, modname, path) |
| 67 if not os.path.isdir(path): |
| 68 return m |
| 69 |
| 70 for subitem in os.listdir(path): |
| 71 subpath = os.path.join(path, subitem) |
| 72 subname = os.path.splitext(subitem)[0] |
| 73 if skip_fn(subname): |
| 74 continue |
| 75 if os.path.isdir(subpath): |
| 76 if not os.path.exists(os.path.join(subpath, '__init__.py')): |
| 77 continue |
| 78 elif not subpath.endswith('.py') or subitem.startswith('__init__.py'): |
| 79 continue |
| 80 |
| 81 submod = recursive_import(subpath, fullname, skip_fn=skip_fn) |
| 82 |
| 83 if not hasattr(m, subname): |
| 84 setattr(m, subname, submod) |
| 85 else: |
| 86 prev = getattr(m, subname) |
| 87 assert submod is prev, ( |
| 88 'Conflicting modules: %s and %s' % (prev, m)) |
| 89 |
| 90 return m |
| 91 |
| 92 imp.acquire_lock() |
| 93 try: |
| 94 if RM not in sys.modules: |
| 95 sys.modules[RM] = imp.new_module(RM) |
| 96 # First import all the APIs and configs |
| 97 for root in mod_dirs: |
| 98 if os.path.isdir(root): |
| 99 recursive_import(root, skip_fn=lambda name: name.endswith('_config')) |
| 100 |
| 101 # Then fixup all the modules |
| 102 for name, submod in sys.modules[RM].__dict__.iteritems(): |
| 103 if name[0] == '_': |
| 104 continue |
| 105 patchup_module(name, submod) |
| 106 |
| 107 # Then import all the config extenders. |
| 108 for root in mod_dirs: |
| 109 if os.path.isdir(root): |
| 110 recursive_import(root) |
| 111 return sys.modules[RM] |
| 112 finally: |
| 113 imp.release_lock() |
| 114 |
| 115 |
| 116 def CreateApi(mod_dirs, names, test_data=DisabledTestData(), required=None, |
| 117 optional=None, kwargs=None): |
| 118 """ |
| 119 Given a list of module names, return an instance of RecipeApi which contains |
| 120 those modules as direct members. |
| 121 |
| 122 So, if you pass ['foobar'], you'll get an instance back which contains a |
| 123 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' |
| 124 module. |
| 125 |
| 126 Args: |
| 127 names (list): A list of module names to include in the returned RecipeApi. |
| 128 mod_dirs (list): A list of paths to directories which contain modules. |
| 129 test_data (TestData): ... |
| 130 kwargs: Data passed to each module api. Usually this will contain: |
| 131 properties (dict): the properties dictionary (used by the properties |
| 132 module) |
| 133 step_history (OrderedDict): the step history object (used by the |
| 134 step_history module!) |
| 135 """ |
| 136 kwargs = kwargs or {} |
| 137 recipe_modules = load_recipe_modules(mod_dirs) |
| 138 |
| 139 inst_maps = {} |
| 140 if required: |
| 141 inst_maps[required[0]] = { None: required[1]() } |
| 142 if optional: |
| 143 inst_maps[optional[0]] = { None: optional[1]() } |
| 144 |
| 145 dep_map = {None: set(names)} |
| 146 def create_maps(name): |
| 147 if name not in dep_map: |
| 148 module = getattr(recipe_modules, name) |
| 149 |
| 150 dep_map[name] = set(module.DEPS) |
| 151 map(create_maps, dep_map[name]) |
| 152 |
| 153 mod_test = DisabledTestData() |
| 154 if test_data.enabled: |
| 155 mod_test = test_data.mod_data.get(name, ModuleTestData()) |
| 156 |
| 157 if required: |
| 158 api = getattr(module, required[0]) |
| 159 inst_maps[required[0]][name] = api(module=module, |
| 160 test_data=mod_test, **kwargs) |
| 161 if optional: |
| 162 api = getattr(module, optional[0], None) or optional[1] |
| 163 inst_maps[optional[0]][name] = api(module=module, |
| 164 test_data=mod_test) |
| 165 |
| 166 map(create_maps, names) |
| 167 |
| 168 if required: |
| 169 MapDependencies(dep_map, inst_maps[required[0]]) |
| 170 if optional: |
| 171 MapDependencies(dep_map, inst_maps[optional[0]]) |
| 172 if required: |
| 173 for name, module in inst_maps[required[0]].iteritems(): |
| 174 module.test_api = inst_maps[optional[0]][name] |
| 175 |
| 176 return inst_maps[(required or optional)[0]][None] |
| 177 |
| 178 |
| 179 def MapDependencies(dep_map, inst_map): |
| 180 # NOTE: this is 'inefficient', but correct and compact. |
| 181 dep_map = copy.deepcopy(dep_map) |
| 182 while dep_map: |
| 183 did_something = False |
| 184 to_pop = [] |
| 185 for api_name, deps in dep_map.iteritems(): |
| 186 to_remove = [] |
| 187 for dep in [d for d in deps if d not in dep_map]: |
| 188 # Grab the injection site |
| 189 obj = inst_map[api_name].m |
| 190 assert not hasattr(obj, dep) |
| 191 setattr(obj, dep, inst_map[dep]) |
| 192 to_remove.append(dep) |
| 193 did_something = True |
| 194 map(deps.remove, to_remove) |
| 195 if not deps: |
| 196 to_pop.append(api_name) |
| 197 did_something = True |
| 198 map(dep_map.pop, to_pop) |
| 199 assert did_something, 'Did nothing on this loop. %s' % dep_map |
| 200 |
| 201 |
| 202 def CreateTestApi(names): |
| 203 return CreateApi(MODULE_DIRS(), names, optional=('TEST_API', RecipeTestApi)) |
| 204 |
| 205 |
| 206 def CreateRecipeApi(names, test_data=DisabledTestData(), **kwargs): |
| 207 return CreateApi(MODULE_DIRS(), names, test_data=test_data, kwargs=kwargs, |
| 208 required=('API', RecipeApi), |
| 209 optional=('TEST_API', RecipeTestApi)) |
| 210 |
| 211 |
| 212 class NoSuchRecipe(Exception): |
| 213 pass |
| 214 |
| 215 |
| 216 def LoadRecipe(recipe): |
| 217 # If the recipe is specified as "module:recipe", then it is an recipe |
| 218 # contained in a recipe_module as an example. Look for it in the modules |
| 219 # imported by load_recipe_modules instead of the normal search paths. |
| 220 if ':' in recipe: |
| 221 module_name, example = recipe.split(':') |
| 222 assert example.endswith('example') |
| 223 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS()) |
| 224 try: |
| 225 return getattr(getattr(RECIPE_MODULES, module_name), example) |
| 226 except AttributeError: |
| 227 pass |
| 228 else: |
| 229 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()): |
| 230 recipe_module = chromium_utils.IsolatedImportFromPath(recipe_path) |
| 231 if recipe_module: |
| 232 return recipe_module |
| 233 raise NoSuchRecipe(recipe) |
| 234 |
| 235 |
| 236 def find_recipes(path, predicate): |
| 237 for root, _dirs, files in os.walk(path): |
| 238 for recipe in (f for f in files if predicate(f)): |
| 239 recipe_path = os.path.join(root, recipe) |
| 240 yield recipe_path |
| 241 |
| 242 |
| 243 def loop_over_recipes(): |
| 244 for path in RECIPE_DIRS(): |
| 245 for recipe in find_recipes( |
| 246 path, lambda f: f.endswith('.py') and f[0] != '_'): |
| 247 yield recipe, recipe[len(path)+1:-len('.py')] |
| 248 for path in MODULE_DIRS(): |
| 249 for recipe in find_recipes( |
| 250 path, lambda f: f.endswith('example.py')): |
| 251 module_name = os.path.dirname(recipe)[len(path)+1:] |
| 252 yield recipe, '%s:example' % module_name |
OLD | NEW |