Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(96)

Side by Side Diff: scripts/slave/recipe_loader.py

Issue 187203005: Minor cleanup of some recipe framework code. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Rietveld >_< Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « scripts/slave/recipe_config.py ('k') | scripts/slave/recipe_modules/step/api.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
OLDNEW
« no previous file with comments | « scripts/slave/recipe_config.py ('k') | scripts/slave/recipe_modules/step/api.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698