Chromium Code Reviews| 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 import copy | 4 import copy |
| 5 import imp | 5 import imp |
| 6 import inspect | 6 import inspect |
| 7 import os | 7 import os |
| 8 import sys | 8 import sys |
| 9 from functools import wraps | 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 |
| 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 |
| 16 def load_recipe_modules(mod_dirs): | 17 def load_recipe_modules(mod_dirs): |
| 17 def patchup_module(name, submod): | 18 def patchup_module(name, submod): |
| 19 """Finds framework related classes and functions in a |submod| and adds | |
|
Vadim Sh.
2014/03/05 06:09:30
That was unclear for me from the first glance.
iannucci
2014/03/05 20:43:37
Thanks. This docstring lg.
| |
| 20 them to |submod| as top level constants with well known names such as | |
| 21 API, CONFIG_CTX and TEST_API. | |
| 22 | |
| 23 |submod| is a recipe module (akin to python package) with submodules such as | |
|
iannucci
2014/03/05 20:43:37
We should find a standard way to document types. S
Vadim Sh.
2014/03/05 21:40:38
With current level of magicallity doc browser woul
iannucci
2014/03/05 21:58:30
Maybe. I think it would be more useful than you mi
| |
| 24 'api', 'config', 'test_api'. This function scans through dicts of that | |
| 25 submodules to find subclasses of RecipeApi, RecipeTestApi, etc. | |
| 26 """ | |
| 18 submod.NAME = name | 27 submod.NAME = name |
| 19 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) | 28 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) |
| 20 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) | 29 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) |
| 21 | 30 |
| 22 if hasattr(submod, 'config'): | 31 if hasattr(submod, 'config'): |
| 23 for v in submod.config.__dict__.itervalues(): | 32 for v in submod.config.__dict__.itervalues(): |
| 24 if hasattr(v, 'I_AM_A_CONFIG_CTX'): | 33 if isinstance(v, ConfigContext): |
|
Vadim Sh.
2014/03/05 06:09:30
That was WTF moment...
iannucci
2014/03/05 20:43:37
:D You're deep into the Old Code here ^_^.
Lookin
| |
| 25 assert not submod.CONFIG_CTX, ( | 34 assert not submod.CONFIG_CTX, ( |
| 26 'More than one configuration context: %s' % (submod.config)) | 35 'More than one configuration context: %s' % (submod.config)) |
| 27 submod.CONFIG_CTX = v | 36 submod.CONFIG_CTX = v |
| 28 assert submod.CONFIG_CTX, 'Config file, but no config context?' | 37 assert submod.CONFIG_CTX, 'Config file, but no config context?' |
| 29 | 38 |
| 30 submod.API = getattr(submod, 'API', None) | 39 submod.API = getattr(submod, 'API', None) |
| 31 for v in submod.api.__dict__.itervalues(): | 40 for v in submod.api.__dict__.itervalues(): |
| 32 if inspect.isclass(v) and issubclass(v, RecipeApi): | 41 if inspect.isclass(v) and issubclass(v, RecipeApi): |
| 33 assert not submod.API, ( | 42 assert not submod.API, ( |
| 34 'More than one Api subclass: %s' % submod.api) | 43 'More than one Api subclass: %s' % submod.api) |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 108 | 117 |
| 109 # Then import all the config extenders. | 118 # Then import all the config extenders. |
| 110 for root in mod_dirs: | 119 for root in mod_dirs: |
| 111 if os.path.isdir(root): | 120 if os.path.isdir(root): |
| 112 recursive_import(root) | 121 recursive_import(root) |
| 113 return sys.modules[RM] | 122 return sys.modules[RM] |
| 114 finally: | 123 finally: |
| 115 imp.release_lock() | 124 imp.release_lock() |
| 116 | 125 |
| 117 | 126 |
| 118 def CreateApi(mod_dirs, names, test_data=DisabledTestData(), required=None, | 127 def create_api(mod_dirs, names, test_data=DisabledTestData(), required=None, |
|
Vadim Sh.
2014/03/05 06:09:30
I really can't stand mixed code styles in a single
Vadim Sh.
2014/03/05 06:09:30
Also this function is a big WTF for me, I still ha
iannucci
2014/03/05 20:43:37
yep. I think this slipped through a codereview
iannucci
2014/03/05 20:43:37
Basically it creates a RecipeApi instance, with al
| |
| 119 optional=None, kwargs=None): | 128 optional=None, kwargs=None): |
| 120 """ | 129 """ |
| 121 Given a list of module names, return an instance of RecipeApi which contains | 130 Given a list of module names, return an instance of RecipeApi which contains |
| 122 those modules as direct members. | 131 those modules as direct members. |
| 123 | 132 |
| 124 So, if you pass ['foobar'], you'll get an instance back which contains a | 133 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' | 134 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' |
| 126 module. | 135 module. |
| 127 | 136 |
| 128 Args: | 137 Args: |
| 138 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. | 139 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): ... | 140 test_data (TestData): ... |
| 141 required: a pair such as ('API', RecipeApi). | |
|
Vadim Sh.
2014/03/05 06:09:30
I really tried to come up with a better doc string
| |
| 142 optional: a pair such as ('TEST_API', RecipeTestApi). | |
| 132 kwargs: Data passed to each module api. Usually this will contain: | 143 kwargs: Data passed to each module api. Usually this will contain: |
| 133 properties (dict): the properties dictionary (used by the properties | 144 properties (dict): the properties dictionary (used by the properties |
| 134 module) | 145 module) |
| 135 step_history (OrderedDict): the step history object (used by the | 146 step_history (OrderedDict): the step history object (used by the |
| 136 step_history module!) | 147 step_history module!) |
| 137 """ | 148 """ |
| 138 kwargs = kwargs or {} | 149 kwargs = kwargs or {} |
| 139 recipe_modules = load_recipe_modules(mod_dirs) | 150 recipe_modules = load_recipe_modules(mod_dirs) |
| 140 | 151 |
| 141 inst_maps = {} | 152 inst_maps = {} |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 155 mod_test = DisabledTestData() | 166 mod_test = DisabledTestData() |
| 156 if test_data.enabled: | 167 if test_data.enabled: |
| 157 mod_test = test_data.mod_data.get(name, ModuleTestData()) | 168 mod_test = test_data.mod_data.get(name, ModuleTestData()) |
| 158 | 169 |
| 159 if required: | 170 if required: |
| 160 api = getattr(module, required[0]) | 171 api = getattr(module, required[0]) |
| 161 inst_maps[required[0]][name] = api(module=module, | 172 inst_maps[required[0]][name] = api(module=module, |
| 162 test_data=mod_test, **kwargs) | 173 test_data=mod_test, **kwargs) |
| 163 if optional: | 174 if optional: |
| 164 api = getattr(module, optional[0], None) or optional[1] | 175 api = getattr(module, optional[0], None) or optional[1] |
| 176 # TODO(vadimsh): Why not pass **kwargs here as well? There's | |
| 177 # an assumption here that optional[1] is always RecipeTestApi subclass | |
| 178 # (that doesn't need kwargs). | |
|
Vadim Sh.
2014/03/05 06:09:30
I dislike this asymmetry...
iannucci
2014/03/05 20:43:37
Yeah me too :(. There shouldn't be kwargs at all.
Vadim Sh.
2014/03/05 21:40:38
It's used by properties and step_history modules.
| |
| 165 inst_maps[optional[0]][name] = api(module=module, | 179 inst_maps[optional[0]][name] = api(module=module, |
| 166 test_data=mod_test) | 180 test_data=mod_test) |
| 167 | 181 |
| 168 map(create_maps, names) | 182 map(create_maps, names) |
| 169 | 183 |
| 170 if required: | 184 if required: |
| 171 MapDependencies(dep_map, inst_maps[required[0]]) | 185 map_dependencies(dep_map, inst_maps[required[0]]) |
| 172 if optional: | 186 if optional: |
| 173 MapDependencies(dep_map, inst_maps[optional[0]]) | 187 map_dependencies(dep_map, inst_maps[optional[0]]) |
| 174 if required: | 188 if required: |
| 175 for name, module in inst_maps[required[0]].iteritems(): | 189 for name, module in inst_maps[required[0]].iteritems(): |
| 176 module.test_api = inst_maps[optional[0]][name] | 190 module.test_api = inst_maps[optional[0]][name] |
| 177 | 191 |
| 178 return inst_maps[(required or optional)[0]][None] | 192 return inst_maps[(required or optional)[0]][None] |
| 179 | 193 |
| 180 | 194 |
| 181 def MapDependencies(dep_map, inst_map): | 195 def map_dependencies(dep_map, inst_map): |
| 182 # NOTE: this is 'inefficient', but correct and compact. | 196 # NOTE: this is 'inefficient', but correct and compact. |
| 183 dep_map = copy.deepcopy(dep_map) | 197 dep_map = copy.deepcopy(dep_map) |
| 184 while dep_map: | 198 while dep_map: |
| 185 did_something = False | 199 did_something = False |
| 186 to_pop = [] | 200 to_pop = [] |
| 187 for api_name, deps in dep_map.iteritems(): | 201 for api_name, deps in dep_map.iteritems(): |
| 188 to_remove = [] | 202 to_remove = [] |
| 189 for dep in [d for d in deps if d not in dep_map]: | 203 for dep in [d for d in deps if d not in dep_map]: |
| 190 # Grab the injection site | 204 # Grab the injection site |
| 191 obj = inst_map[api_name].m | 205 obj = inst_map[api_name].m |
| 192 assert not hasattr(obj, dep) | 206 assert not hasattr(obj, dep) |
| 193 setattr(obj, dep, inst_map[dep]) | 207 setattr(obj, dep, inst_map[dep]) |
| 194 to_remove.append(dep) | 208 to_remove.append(dep) |
| 195 did_something = True | 209 did_something = True |
| 196 map(deps.remove, to_remove) | 210 map(deps.remove, to_remove) |
| 197 if not deps: | 211 if not deps: |
| 198 to_pop.append(api_name) | 212 to_pop.append(api_name) |
| 199 did_something = True | 213 did_something = True |
| 200 map(dep_map.pop, to_pop) | 214 map(dep_map.pop, to_pop) |
| 201 assert did_something, 'Did nothing on this loop. %s' % dep_map | 215 assert did_something, 'Did nothing on this loop. %s' % dep_map |
| 202 | 216 |
| 203 | 217 |
| 204 def CreateTestApi(names): | 218 def create_test_api(names): |
| 205 return CreateApi(MODULE_DIRS(), names, optional=('TEST_API', RecipeTestApi)) | 219 return create_api(MODULE_DIRS(), names, optional=('TEST_API', RecipeTestApi)) |
| 206 | 220 |
| 207 | 221 |
| 208 def CreateRecipeApi(names, test_data=DisabledTestData(), **kwargs): | 222 def create_recipe_api(names, test_data=DisabledTestData(), **kwargs): |
| 209 return CreateApi(MODULE_DIRS(), names, test_data=test_data, kwargs=kwargs, | 223 return create_api(MODULE_DIRS(), names, test_data=test_data, kwargs=kwargs, |
| 210 required=('API', RecipeApi), | 224 required=('API', RecipeApi), |
| 211 optional=('TEST_API', RecipeTestApi)) | 225 optional=('TEST_API', RecipeTestApi)) |
| 212 | 226 |
| 213 | 227 |
| 214 def Cached(f): | 228 def cached(f): |
|
iannucci
2014/03/05 20:43:37
maybe this should move to recipe_util.py?
Probabl
Vadim Sh.
2014/03/05 21:40:38
Done.
| |
| 215 """Caches/memoizes a unary function. | 229 """Caches/memoizes an unary function. |
| 216 | 230 |
| 217 If the function throws an exception, the cache table will not be updated. | 231 If the function throws an exception, the cache table will not be updated. |
| 218 """ | 232 """ |
| 219 cache = {} | 233 cache = {} |
| 220 empty = object() | 234 empty = object() |
| 221 | 235 |
| 222 @wraps(f) | 236 @wraps(f) |
| 223 def cached_f(inp): | 237 def cached_f(inp): |
| 224 cache_entry = cache.get(inp, empty) | 238 cache_entry = cache.get(inp, empty) |
| 225 if cache_entry is empty: | 239 if cache_entry is empty: |
| 226 cache_entry = f(inp) | 240 cache_entry = f(inp) |
| 227 cache[inp] = cache_entry | 241 cache[inp] = cache_entry |
| 228 return cache_entry | 242 return cache_entry |
| 229 return cached_f | 243 return cached_f |
| 230 | 244 |
| 231 | 245 |
| 232 class NoSuchRecipe(Exception): | 246 class NoSuchRecipe(Exception): |
| 233 pass | 247 pass |
| 234 | 248 |
| 235 | 249 |
| 236 class ModuleObject(object): | 250 class ModuleObject(object): |
| 237 def __init__(self, d): | 251 def __init__(self, d): |
| 238 for k, v in d.iteritems(): | 252 for k, v in d.iteritems(): |
| 239 setattr(self, k, v) | 253 setattr(self, k, v) |
| 240 | 254 |
| 241 | 255 |
| 242 @Cached | 256 @cached |
| 243 def LoadRecipe(recipe): | 257 def load_recipe(recipe): |
| 244 # If the recipe is specified as "module:recipe", then it is an recipe | 258 # 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 | 259 # 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. | 260 # imported by load_recipe_modules instead of the normal search paths. |
| 247 if ':' in recipe: | 261 if ':' in recipe: |
| 248 module_name, example = recipe.split(':') | 262 module_name, example = recipe.split(':') |
| 249 assert example.endswith('example') | 263 assert example.endswith('example') |
| 250 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS()) | 264 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS()) |
| 251 try: | 265 try: |
| 252 return getattr(getattr(RECIPE_MODULES, module_name), example) | 266 return getattr(getattr(RECIPE_MODULES, module_name), example) |
| 253 except AttributeError: | 267 except AttributeError: |
| (...skipping 21 matching lines...) Expand all Loading... | |
| 275 def loop_over_recipes(): | 289 def loop_over_recipes(): |
| 276 for path in RECIPE_DIRS(): | 290 for path in RECIPE_DIRS(): |
| 277 for recipe in find_recipes( | 291 for recipe in find_recipes( |
| 278 path, lambda f: f.endswith('.py') and f[0] != '_'): | 292 path, lambda f: f.endswith('.py') and f[0] != '_'): |
| 279 yield recipe, recipe[len(path)+1:-len('.py')] | 293 yield recipe, recipe[len(path)+1:-len('.py')] |
| 280 for path in MODULE_DIRS(): | 294 for path in MODULE_DIRS(): |
| 281 for recipe in find_recipes( | 295 for recipe in find_recipes( |
| 282 path, lambda f: f.endswith('example.py')): | 296 path, lambda f: f.endswith('example.py')): |
| 283 module_name = os.path.dirname(recipe)[len(path)+1:] | 297 module_name = os.path.dirname(recipe)[len(path)+1:] |
| 284 yield recipe, '%s:example' % module_name | 298 yield recipe, '%s:example' % module_name |
| OLD | NEW |