| OLD | NEW |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 |
| 4 import copy | 5 import copy |
| 5 import imp | 6 import imp |
| 6 import inspect | 7 import inspect |
| 7 import os | 8 import os |
| 8 import sys | 9 import sys |
| 9 from functools import wraps | |
| 10 | 10 |
| 11 from .recipe_util import RECIPE_DIRS, MODULE_DIRS | 11 from .recipe_util import RECIPE_DIRS, MODULE_DIRS, cached_unary, scan_directory |
| 12 from .recipe_api import RecipeApi | 12 from .recipe_api import RecipeApi |
| 13 from .recipe_config import ConfigContext |
| 13 from .recipe_test_api import RecipeTestApi, DisabledTestData, ModuleTestData | 14 from .recipe_test_api import RecipeTestApi, DisabledTestData, ModuleTestData |
| 14 | 15 |
| 15 | 16 |
| 17 class NoSuchRecipe(Exception): |
| 18 """Raised by load_recipe is recipe is not found.""" |
| 19 |
| 20 |
| 21 class RecipeScript(object): |
| 22 """Holds dict of an evaluated recipe script.""" |
| 23 |
| 24 def __init__(self, recipe_dict): |
| 25 for k, v in recipe_dict.iteritems(): |
| 26 setattr(self, k, v) |
| 27 |
| 28 @classmethod |
| 29 def from_script_path(cls, script_path): |
| 30 """Evaluates a script and returns RecipeScript instance.""" |
| 31 script_vars = {} |
| 32 execfile(script_path, script_vars) |
| 33 return cls(script_vars) |
| 34 |
| 35 @classmethod |
| 36 def from_module_object(cls, module_obj): |
| 37 """Converts python module object into RecipeScript instance.""" |
| 38 return cls(module_obj.__dict__) |
| 39 |
| 40 |
| 16 def load_recipe_modules(mod_dirs): | 41 def load_recipe_modules(mod_dirs): |
| 42 """Makes a python module object that have all recipe modules in its dict. |
| 43 |
| 44 Args: |
| 45 mod_dirs (list of str): list of module search paths. |
| 46 """ |
| 17 def patchup_module(name, submod): | 47 def patchup_module(name, submod): |
| 48 """Finds framework related classes and functions in a |submod| and adds |
| 49 them to |submod| as top level constants with well known names such as |
| 50 API, CONFIG_CTX and TEST_API. |
| 51 |
| 52 |submod| is a recipe module (akin to python package) with submodules such as |
| 53 'api', 'config', 'test_api'. This function scans through dicts of that |
| 54 submodules to find subclasses of RecipeApi, RecipeTestApi, etc. |
| 55 """ |
| 18 submod.NAME = name | 56 submod.NAME = name |
| 19 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) | 57 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) |
| 20 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) | 58 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) |
| 21 | 59 |
| 22 if hasattr(submod, 'config'): | 60 if hasattr(submod, 'config'): |
| 23 for v in submod.config.__dict__.itervalues(): | 61 for v in submod.config.__dict__.itervalues(): |
| 24 if hasattr(v, 'I_AM_A_CONFIG_CTX'): | 62 if isinstance(v, ConfigContext): |
| 25 assert not submod.CONFIG_CTX, ( | 63 assert not submod.CONFIG_CTX, ( |
| 26 'More than one configuration context: %s' % (submod.config)) | 64 'More than one configuration context: %s' % (submod.config)) |
| 27 submod.CONFIG_CTX = v | 65 submod.CONFIG_CTX = v |
| 28 assert submod.CONFIG_CTX, 'Config file, but no config context?' | 66 assert submod.CONFIG_CTX, 'Config file, but no config context?' |
| 29 | 67 |
| 30 submod.API = getattr(submod, 'API', None) | 68 submod.API = getattr(submod, 'API', None) |
| 31 for v in submod.api.__dict__.itervalues(): | 69 for v in submod.api.__dict__.itervalues(): |
| 32 if inspect.isclass(v) and issubclass(v, RecipeApi): | 70 if inspect.isclass(v) and issubclass(v, RecipeApi): |
| 33 assert not submod.API, ( | 71 assert not submod.API, ( |
| 34 'More than one Api subclass: %s' % submod.api) | 72 'More than one Api subclass: %s' % submod.api) |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 108 | 146 |
| 109 # Then import all the config extenders. | 147 # Then import all the config extenders. |
| 110 for root in mod_dirs: | 148 for root in mod_dirs: |
| 111 if os.path.isdir(root): | 149 if os.path.isdir(root): |
| 112 recursive_import(root) | 150 recursive_import(root) |
| 113 return sys.modules[RM] | 151 return sys.modules[RM] |
| 114 finally: | 152 finally: |
| 115 imp.release_lock() | 153 imp.release_lock() |
| 116 | 154 |
| 117 | 155 |
| 118 def CreateApi(mod_dirs, names, test_data=DisabledTestData(), required=None, | 156 def create_api(mod_dirs, names, test_data=DisabledTestData(), required=None, |
| 119 optional=None, kwargs=None): | 157 optional=None, kwargs=None): |
| 120 """ | 158 """Given a list of module names, return an instance of RecipeApi which |
| 121 Given a list of module names, return an instance of RecipeApi which contains | 159 contains those modules as direct members. |
| 122 those modules as direct members. | |
| 123 | 160 |
| 124 So, if you pass ['foobar'], you'll get an instance back which contains a | 161 So, if you pass ['foobar'], you'll get an instance back which contains a |
| 125 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' | 162 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' |
| 126 module. | 163 module. |
| 127 | 164 |
| 128 Args: | 165 Args: |
| 166 mod_dirs (list): A list of paths to directories which contain modules. |
| 129 names (list): A list of module names to include in the returned RecipeApi. | 167 names (list): A list of module names to include in the returned RecipeApi. |
| 130 mod_dirs (list): A list of paths to directories which contain modules. | |
| 131 test_data (TestData): ... | 168 test_data (TestData): ... |
| 169 required: a pair such as ('API', RecipeApi). |
| 170 optional: a pair such as ('TEST_API', RecipeTestApi). |
| 132 kwargs: Data passed to each module api. Usually this will contain: | 171 kwargs: Data passed to each module api. Usually this will contain: |
| 133 properties (dict): the properties dictionary (used by the properties | 172 properties (dict): the properties dictionary (used by the properties |
| 134 module) | 173 module) |
| 135 step_history (OrderedDict): the step history object (used by the | 174 step_history (OrderedDict): the step history object (used by the |
| 136 step_history module!) | 175 step_history module!) |
| 137 """ | 176 """ |
| 138 kwargs = kwargs or {} | 177 kwargs = kwargs or {} |
| 139 recipe_modules = load_recipe_modules(mod_dirs) | 178 recipe_modules = load_recipe_modules(mod_dirs) |
| 140 | 179 |
| 141 inst_maps = {} | 180 inst_maps = {} |
| (...skipping 13 matching lines...) Expand all Loading... |
| 155 mod_test = DisabledTestData() | 194 mod_test = DisabledTestData() |
| 156 if test_data.enabled: | 195 if test_data.enabled: |
| 157 mod_test = test_data.mod_data.get(name, ModuleTestData()) | 196 mod_test = test_data.mod_data.get(name, ModuleTestData()) |
| 158 | 197 |
| 159 if required: | 198 if required: |
| 160 api = getattr(module, required[0]) | 199 api = getattr(module, required[0]) |
| 161 inst_maps[required[0]][name] = api(module=module, | 200 inst_maps[required[0]][name] = api(module=module, |
| 162 test_data=mod_test, **kwargs) | 201 test_data=mod_test, **kwargs) |
| 163 if optional: | 202 if optional: |
| 164 api = getattr(module, optional[0], None) or optional[1] | 203 api = getattr(module, optional[0], None) or optional[1] |
| 204 # TODO(vadimsh): Why not pass **kwargs here as well? There's |
| 205 # an assumption here that optional[1] is always RecipeTestApi subclass |
| 206 # (that doesn't need kwargs). |
| 165 inst_maps[optional[0]][name] = api(module=module, | 207 inst_maps[optional[0]][name] = api(module=module, |
| 166 test_data=mod_test) | 208 test_data=mod_test) |
| 167 | 209 |
| 168 map(create_maps, names) | 210 map(create_maps, names) |
| 169 | 211 |
| 170 if required: | 212 if required: |
| 171 MapDependencies(dep_map, inst_maps[required[0]]) | 213 map_dependencies(dep_map, inst_maps[required[0]]) |
| 172 if optional: | 214 if optional: |
| 173 MapDependencies(dep_map, inst_maps[optional[0]]) | 215 map_dependencies(dep_map, inst_maps[optional[0]]) |
| 174 if required: | 216 if required: |
| 175 for name, module in inst_maps[required[0]].iteritems(): | 217 for name, module in inst_maps[required[0]].iteritems(): |
| 176 module.test_api = inst_maps[optional[0]][name] | 218 module.test_api = inst_maps[optional[0]][name] |
| 177 | 219 |
| 178 return inst_maps[(required or optional)[0]][None] | 220 return inst_maps[(required or optional)[0]][None] |
| 179 | 221 |
| 180 | 222 |
| 181 def MapDependencies(dep_map, inst_map): | 223 def map_dependencies(dep_map, inst_map): |
| 182 # NOTE: this is 'inefficient', but correct and compact. | 224 # NOTE: this is 'inefficient', but correct and compact. |
| 183 dep_map = copy.deepcopy(dep_map) | 225 dep_map = copy.deepcopy(dep_map) |
| 184 while dep_map: | 226 while dep_map: |
| 185 did_something = False | 227 did_something = False |
| 186 to_pop = [] | 228 to_pop = [] |
| 187 for api_name, deps in dep_map.iteritems(): | 229 for api_name, deps in dep_map.iteritems(): |
| 188 to_remove = [] | 230 to_remove = [] |
| 189 for dep in [d for d in deps if d not in dep_map]: | 231 for dep in [d for d in deps if d not in dep_map]: |
| 190 # Grab the injection site | 232 # Grab the injection site |
| 191 obj = inst_map[api_name].m | 233 obj = inst_map[api_name].m |
| 192 assert not hasattr(obj, dep) | 234 assert not hasattr(obj, dep) |
| 193 setattr(obj, dep, inst_map[dep]) | 235 setattr(obj, dep, inst_map[dep]) |
| 194 to_remove.append(dep) | 236 to_remove.append(dep) |
| 195 did_something = True | 237 did_something = True |
| 196 map(deps.remove, to_remove) | 238 map(deps.remove, to_remove) |
| 197 if not deps: | 239 if not deps: |
| 198 to_pop.append(api_name) | 240 to_pop.append(api_name) |
| 199 did_something = True | 241 did_something = True |
| 200 map(dep_map.pop, to_pop) | 242 map(dep_map.pop, to_pop) |
| 201 assert did_something, 'Did nothing on this loop. %s' % dep_map | 243 assert did_something, 'Did nothing on this loop. %s' % dep_map |
| 202 | 244 |
| 203 | 245 |
| 204 def CreateTestApi(names): | 246 def create_test_api(names): |
| 205 return CreateApi(MODULE_DIRS(), names, optional=('TEST_API', RecipeTestApi)) | 247 return create_api(MODULE_DIRS(), names, optional=('TEST_API', RecipeTestApi)) |
| 206 | 248 |
| 207 | 249 |
| 208 def CreateRecipeApi(names, test_data=DisabledTestData(), **kwargs): | 250 def create_recipe_api(names, test_data=DisabledTestData(), **kwargs): |
| 209 return CreateApi(MODULE_DIRS(), names, test_data=test_data, kwargs=kwargs, | 251 return create_api(MODULE_DIRS(), names, test_data=test_data, kwargs=kwargs, |
| 210 required=('API', RecipeApi), | 252 required=('API', RecipeApi), |
| 211 optional=('TEST_API', RecipeTestApi)) | 253 optional=('TEST_API', RecipeTestApi)) |
| 212 | 254 |
| 213 | 255 |
| 214 def Cached(f): | 256 @cached_unary |
| 215 """Caches/memoizes a unary function. | 257 def load_recipe(recipe): |
| 258 """Given name of a recipe, loads and returns it as RecipeScript instance. |
| 216 | 259 |
| 217 If the function throws an exception, the cache table will not be updated. | 260 Args: |
| 261 recipe (str): name of a recipe, can be in form '<module>:<recipe>'. |
| 262 |
| 263 Returns: |
| 264 RecipeScript instance. |
| 265 |
| 266 Raises: |
| 267 NoSuchRecipe: recipe is not found. |
| 218 """ | 268 """ |
| 219 cache = {} | |
| 220 empty = object() | |
| 221 | |
| 222 @wraps(f) | |
| 223 def cached_f(inp): | |
| 224 cache_entry = cache.get(inp, empty) | |
| 225 if cache_entry is empty: | |
| 226 cache_entry = f(inp) | |
| 227 cache[inp] = cache_entry | |
| 228 return cache_entry | |
| 229 return cached_f | |
| 230 | |
| 231 | |
| 232 class NoSuchRecipe(Exception): | |
| 233 pass | |
| 234 | |
| 235 | |
| 236 class ModuleObject(object): | |
| 237 def __init__(self, d): | |
| 238 for k, v in d.iteritems(): | |
| 239 setattr(self, k, v) | |
| 240 | |
| 241 | |
| 242 @Cached | |
| 243 def LoadRecipe(recipe): | |
| 244 # If the recipe is specified as "module:recipe", then it is an recipe | 269 # If the recipe is specified as "module:recipe", then it is an recipe |
| 245 # contained in a recipe_module as an example. Look for it in the modules | 270 # contained in a recipe_module as an example. Look for it in the modules |
| 246 # imported by load_recipe_modules instead of the normal search paths. | 271 # imported by load_recipe_modules instead of the normal search paths. |
| 247 if ':' in recipe: | 272 if ':' in recipe: |
| 248 module_name, example = recipe.split(':') | 273 module_name, example = recipe.split(':') |
| 249 assert example.endswith('example') | 274 assert example.endswith('example') |
| 250 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS()) | 275 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS()) |
| 251 try: | 276 try: |
| 252 return getattr(getattr(RECIPE_MODULES, module_name), example) | 277 script_module = getattr(getattr(RECIPE_MODULES, module_name), example) |
| 278 return RecipeScript.from_module_object(script_module) |
| 253 except AttributeError: | 279 except AttributeError: |
| 254 raise NoSuchRecipe(recipe, | 280 raise NoSuchRecipe(recipe, |
| 255 'Recipe module %s does not have example %s defined' % | 281 'Recipe module %s does not have example %s defined' % |
| 256 (module_name, example)) | 282 (module_name, example)) |
| 257 else: | 283 else: |
| 258 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()): | 284 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()): |
| 259 script_vars = {} | 285 if os.path.exists(recipe_path + '.py'): |
| 260 script_name = recipe_path + '.py' | 286 return RecipeScript.from_script_path(recipe_path + '.py') |
| 261 if os.path.exists(script_name): | |
| 262 execfile(script_name, script_vars) | |
| 263 loaded_recipe = ModuleObject(script_vars) | |
| 264 return loaded_recipe | |
| 265 raise NoSuchRecipe(recipe) | 287 raise NoSuchRecipe(recipe) |
| 266 | 288 |
| 267 | 289 |
| 268 def find_recipes(path, predicate): | 290 def loop_over_recipes(): |
| 269 for root, _dirs, files in os.walk(path): | 291 """Yields pairs (path to recipe, recipe name). |
| 270 for recipe in (f for f in files if predicate(f)): | |
| 271 recipe_path = os.path.join(root, recipe) | |
| 272 yield recipe_path | |
| 273 | 292 |
| 274 | 293 Enumerates real recipes in recipes/* as well as examples in recipe_modules/*. |
| 275 def loop_over_recipes(): | 294 """ |
| 276 for path in RECIPE_DIRS(): | 295 for path in RECIPE_DIRS(): |
| 277 for recipe in find_recipes( | 296 for recipe in scan_directory( |
| 278 path, lambda f: f.endswith('.py') and f[0] != '_'): | 297 path, lambda f: f.endswith('.py') and f[0] != '_'): |
| 279 yield recipe, recipe[len(path)+1:-len('.py')] | 298 yield recipe, recipe[len(path)+1:-len('.py')] |
| 280 for path in MODULE_DIRS(): | 299 for path in MODULE_DIRS(): |
| 281 for recipe in find_recipes( | 300 for recipe in scan_directory( |
| 282 path, lambda f: f.endswith('example.py')): | 301 path, lambda f: f.endswith('example.py')): |
| 283 module_name = os.path.dirname(recipe)[len(path)+1:] | 302 module_name = os.path.dirname(recipe)[len(path)+1:] |
| 284 yield recipe, '%s:example' % module_name | 303 yield recipe, '%s:example' % module_name |
| OLD | NEW |