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