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

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

Issue 1111413005: Some changes to allow recipes and modules to live noncentrally (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Little bitty cleanup Created 5 years, 7 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
OLDNEW
1 # Copyright 2013 The Chromium Authors. All rights reserved. 1 # Copyright 2013 The Chromium Authors. All rights reserved.
iannucci 2015/05/05 23:35:59 discussed: A given recipe body of code (modules,
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
5 import copy
6 import imp 5 import imp
7 import inspect 6 import inspect
8 import os 7 import os
9 import sys 8 import sys
10 9
11 from .recipe_util import (ROOT_PATH, RECIPE_DIRS, MODULE_DIRS, 10 from .recipe_util import (ROOT_PATH, RECIPE_DIRS, MODULE_DIRS,
12 cached_unary, scan_directory) 11 cached_unary, scan_directory)
13 from .recipe_api import RecipeApi, RecipeApiPlain 12 from .recipe_api import RecipeApi, RecipeApiPlain
14 from .recipe_config import ConfigContext 13 from .recipe_config import ConfigContext
15 from .recipe_config_types import Path, ModuleBasePath 14 from .recipe_config_types import Path, ModuleBasePath, RECIPE_MODULE_PREFIX
16 from .recipe_test_api import RecipeTestApi, DisabledTestData 15 from .recipe_test_api import RecipeTestApi, DisabledTestData
17 16
18 17
19 class NoSuchRecipe(Exception): 18 class NoSuchRecipe(Exception):
20 """Raised by load_recipe is recipe is not found.""" 19 """Raised by load_recipe is recipe is not found."""
21 20
22 21
23 class RecipeScript(object): 22 class RecipeScript(object):
24 """Holds dict of an evaluated recipe script.""" 23 """Holds dict of an evaluated recipe script."""
25 24
26 def __init__(self, recipe_dict): 25 def __init__(self, recipe_dict, loader):
iannucci 2015/05/05 23:35:59 don't use it?
luqui 2015/05/06 22:18:39 Done.
27 for k, v in recipe_dict.iteritems(): 26 for k, v in recipe_dict.iteritems():
28 setattr(self, k, v) 27 setattr(self, k, v)
29 28
30 @classmethod 29 @classmethod
31 def from_script_path(cls, script_path): 30 def from_script_path(cls, script_path, loader):
32 """Evaluates a script and returns RecipeScript instance.""" 31 """Evaluates a script and returns RecipeScript instance."""
33 script_vars = {} 32 script_vars = {}
34 execfile(script_path, script_vars) 33 execfile(script_path, script_vars)
35 script_vars['__file__'] = script_path 34 script_vars['__file__'] = script_path
36 return cls(script_vars) 35 script_vars['LOADED_DEPS'] = deps_from_mixed(
37 36 script_vars.get('DEPS', []), os.path.basename(script_path), loader)
38 @classmethod 37 return cls(script_vars, loader)
39 def from_module_object(cls, module_obj):
40 """Converts python module object into RecipeScript instance."""
41 return cls(module_obj.__dict__)
42 38
43 39
44 def load_recipe_modules(mod_dirs): 40 class PathDependency(object):
iannucci 2015/05/05 23:35:59 let's make a Dependency interface.
luqui 2015/05/06 22:18:39 Done.
45 """Makes a python module object that have all recipe modules in its dict. 41 def __init__(self, path, local_name, base_path=None):
42 self._path = _normalize_path(base_path, path)
43 self._local_name = local_name
46 44
47 Args: 45 # We forbid modules from living outside our main paths to keep clients
48 mod_dirs (list of str): list of module search paths. 46 # from going crazy before we have standardized recipe locations.
49 """ 47 mod_dir = os.path.dirname(path)
50 def patchup_module(name, submod): 48 assert mod_dir in MODULE_DIRS(), (
51 """Finds framework related classes and functions in a |submod| and adds 49 'Modules living outside of approved directories are forbidden: '
52 them to |submod| as top level constants with well known names such as 50 '%s is not in %s' % (mod_dir, MODULE_DIRS()))
53 API, CONFIG_CTX and TEST_API.
54 51
55 |submod| is a recipe module (akin to python package) with submodules such as 52 def load(self, cache):
56 'api', 'config', 'test_api'. This function scans through dicts of that 53 return _load_recipe_module_module(self._path, cache)
57 submodules to find subclasses of RecipeApi, RecipeTestApi, etc.
58 """
59 submod.NAME = name
60 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod))
61 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None)
62 submod.DEPS = frozenset(getattr(submod, 'DEPS', ()))
63 54
64 if hasattr(submod, 'config'): 55 @property
65 for v in submod.config.__dict__.itervalues(): 56 def local_name(self):
66 if isinstance(v, ConfigContext): 57 return self._local_name
67 assert not submod.CONFIG_CTX, (
68 'More than one configuration context: %s' % (submod.config))
69 submod.CONFIG_CTX = v
70 assert submod.CONFIG_CTX, 'Config file, but no config context?'
71 58
72 submod.API = getattr(submod, 'API', None) 59 @property
73 for v in submod.api.__dict__.itervalues(): 60 def unique_name(self):
74 if inspect.isclass(v) and issubclass(v, RecipeApiPlain): 61 """A unique identifier for the module that this dependency refers to.
75 assert not submod.API, ( 62 This must be generated without loading the module. For now it's just
76 'More than one Api subclass: %s' % submod.api) 63 the canonical filesystem path, but eventually we will use a Luci-config
77 submod.API = v 64 identifier."""
78 assert submod.API, 'Submodule has no api? %s' % (submod) 65 return self._path
79 66
80 submod.TEST_API = getattr(submod, 'TEST_API', None)
81 if hasattr(submod, 'test_api'):
82 for v in submod.test_api.__dict__.itervalues():
83 if inspect.isclass(v) and issubclass(v, RecipeTestApi):
84 assert not submod.TEST_API, (
85 'More than one TestApi subclass: %s' % submod.api)
86 submod.TEST_API = v
87 assert submod.API, (
88 'Submodule has test_api.py but no TestApi subclass? %s'
89 % (submod)
90 )
91 67
92 RM = 'RECIPE_MODULES' 68 def NamedDependency(name):
93 def find_and_load(fullname, modname, path): 69 for path in MODULE_DIRS():
94 if fullname not in sys.modules or fullname == RM: 70 mod_path = os.path.join(path, name)
71 if os.path.exists(os.path.join(mod_path, '__init__.py')):
72 return PathDependency(mod_path, name)
73 raise NoSuchRecipe('Recipe module named %s does not exist' % name)
74
75
76 class ModuleLoader(object):
iannucci 2015/05/05 23:35:59 api nitpicks: convenience methods below modify th
luqui 2015/05/06 22:18:39 Done.
77 def __init__(self):
78 self._cache = {}
79
80 def load(self, dep):
81 name = dep.unique_name
82 if name in self._cache:
83 mod = self._cache[name]
84 assert mod is not None, (
85 'Cyclic dependency when trying to load %s' % name)
86 return mod
87 else:
88 self._cache[name] = None
89 mod = dep.load(self)
90 self._cache[name] = mod
91 return mod
92
93
94 def _normalize_path(base_path, path):
95 if base_path is None or os.path.isabs(path):
96 return os.path.realpath(path)
97 else:
98 return os.path.realpath(os.path.join(base_path, path))
99
100
101 def deps_from_names(deps, loader):
102 """Load dependencies given a list simple module names (old style)."""
103 return { dep: loader.load(NamedDependency(dep)) for dep in deps }
104
105
106 def deps_from_paths(deps, base_path, loader):
107 """Load dependencies given a dictionary of local names to module paths
108 (new style)."""
109 return { name: loader.load(PathDependency(path, name, base_path))
110 for name, path in deps.iteritems() }
111
112
113 def deps_from_mixed(deps, base_path, loader):
114 """Load dependencies given either a new style or old style deps spec."""
115 if isinstance(deps, (list, tuple)):
116 return deps_from_names(deps, loader)
117 elif isinstance(deps, dict):
118 return deps_from_paths(deps, base_path, loader)
119 else:
120 raise ValueError('%s is not a valid or known deps structure' % deps)
121
122
123 def _find_and_load_module(fullname, modname, path):
124 imp.acquire_lock()
125 try:
126 if fullname not in sys.modules:
95 fil = None 127 fil = None
96 try: 128 try:
97 fil, pathname, descr = imp.find_module(modname, 129 fil, pathname, descr = imp.find_module(modname,
98 [os.path.dirname(path)]) 130 [os.path.dirname(path)])
99 imp.load_module(fullname, fil, pathname, descr) 131 imp.load_module(fullname, fil, pathname, descr)
100 finally: 132 finally:
101 if fil: 133 if fil:
102 fil.close() 134 fil.close()
103 return sys.modules[fullname] 135 return sys.modules[fullname]
104
105 def recursive_import(path, prefix=None, skip_fn=lambda name: False):
106 modname = os.path.splitext(os.path.basename(path))[0]
107 if prefix:
108 fullname = '%s.%s' % (prefix, modname)
109 else:
110 fullname = RM
111 m = find_and_load(fullname, modname, path)
112 if not os.path.isdir(path):
113 return m
114
115 for subitem in os.listdir(path):
116 subpath = os.path.join(path, subitem)
117 subname = os.path.splitext(subitem)[0]
118 if skip_fn(subname):
119 continue
120 if os.path.isdir(subpath):
121 if not os.path.exists(os.path.join(subpath, '__init__.py')):
122 continue
123 elif not subpath.endswith('.py') or subitem.startswith('__init__.py'):
124 continue
125
126 submod = recursive_import(subpath, fullname, skip_fn=skip_fn)
127
128 if not hasattr(m, subname):
129 setattr(m, subname, submod)
130 else:
131 prev = getattr(m, subname)
132 assert submod is prev, (
133 'Conflicting modules: %s and %s' % (prev, m))
134
135 return m
136
137 imp.acquire_lock()
138 try:
139 if RM not in sys.modules:
140 sys.modules[RM] = imp.new_module(RM)
141 # First import all the APIs and configs
142 for root in mod_dirs:
143 if os.path.isdir(root):
144 recursive_import(root, skip_fn=lambda name: name.endswith('_config'))
145
146 # Then fixup all the modules
147 for name, submod in sys.modules[RM].__dict__.iteritems():
148 if name[0] == '_':
149 continue
150 patchup_module(name, submod)
151
152 # Then import all the config extenders.
153 for root in mod_dirs:
154 if os.path.isdir(root):
155 recursive_import(root)
156 return sys.modules[RM]
157 finally: 136 finally:
158 imp.release_lock() 137 imp.release_lock()
159 138
160 139
161 def create_apis(mod_dirs, names, only_test_api, engine, test_data): 140 def _load_recipe_module_module(path, loader):
162 """Given a list of module names, return linked instances of RecipeApi 141 imp.acquire_lock()
163 and RecipeTestApi (in a pair) which contains those modules as direct members. 142 try:
143 if RECIPE_MODULE_PREFIX not in sys.modules:
144 sys.modules[RECIPE_MODULE_PREFIX] = imp.new_module(RECIPE_MODULE_PREFIX)
145 finally:
146 imp.release_lock()
164 147
165 So, if you pass ['foobar'], you'll get an instance back which contains a 148 modname = os.path.splitext(os.path.basename(path))[0]
166 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' 149 fullname = '%s.%s' % (RECIPE_MODULE_PREFIX, modname)
167 module. 150 mod = _find_and_load_module(fullname, modname, path)
168 151
169 Args: 152 # This actually loads the dependencies.
170 mod_dirs (list): A list of paths to directories which contain modules. 153 mod.LOADED_DEPS = deps_from_mixed(getattr(mod, 'DEPS', []),
171 names (list): A list of module names to include in the returned RecipeApi. 154 os.path.basename(path), loader)
172 only_test_api (bool): If True, do not create RecipeApi, only RecipeTestApi.
173 engine (object): A recipe engine instance that gets passed to each API.
174 Among other things it provides:
175 properties (dict): the properties dictionary (used by the properties
176 module)
177 See annotated_run.py for definition.
178 test_data (TestData): ...
179 155
180 Returns: 156 # TODO(luqui): Remove this hack once configs are cleaned.
181 Pair (RecipeApi instance or None, RecipeTestApi instance). 157 sys.modules['%s.DEPS' % fullname] = mod.LOADED_DEPS
182 """ 158 _recursive_import(path, RECIPE_MODULE_PREFIX)
183 recipe_modules = load_recipe_modules(mod_dirs) 159 _patchup_module(modname, mod)
184 160
185 # Recipe module name (or None for top level API) -> RecipeTestApi instance. 161 return mod
186 test_apis = {}
187 # Recipe module name (or None for top level API) -> RecipeApi instance.
188 apis = {}
189
190 # 'None' keys represent top level API objects returned by this function.
191 test_apis[None] = RecipeTestApi(module=None)
192 if not only_test_api:
193 apis[None] = RecipeApi(module=None,
194 engine=engine,
195 test_data=test_data.get_module_test_data(None))
196
197 dep_map = {None: set(names)}
198 def create_maps(name):
199 if name not in dep_map:
200 module = getattr(recipe_modules, name)
201
202 dep_map[name] = set(module.DEPS)
203 map(create_maps, dep_map[name])
204
205 test_api_cls = getattr(module, 'TEST_API', None) or RecipeTestApi
206 test_apis[name] = test_api_cls(module=module)
207
208 if not only_test_api:
209 api_cls = getattr(module, 'API')
210 apis[name] = api_cls(module=module,
211 engine=engine,
212 test_data=test_data.get_module_test_data(name))
213
214 map(create_maps, names)
215
216 map_dependencies(dep_map, test_apis)
217 if not only_test_api:
218 map_dependencies(dep_map, apis)
219 for name, module in apis.iteritems():
220 module.test_api = test_apis[name]
221
222 return apis.get(None), test_apis.get(None)
223 162
224 163
225 def map_dependencies(dep_map, inst_map): 164 def _recursive_import(path, prefix):
226 # NOTE: this is 'inefficient', but correct and compact. 165 modname = os.path.splitext(os.path.basename(path))[0]
227 dep_map = copy.deepcopy(dep_map) 166 fullname = '%s.%s' % (prefix, modname)
228 while dep_map: 167 mod = _find_and_load_module(fullname, modname, path)
229 did_something = False 168 if not os.path.isdir(path):
230 to_pop = [] 169 return mod
231 for api_name, deps in dep_map.iteritems(): 170
232 to_remove = [] 171 for subitem in os.listdir(path):
233 for dep in [d for d in deps if d not in dep_map]: 172 subpath = os.path.join(path, subitem)
234 # Grab the injection site 173 subname = os.path.splitext(subitem)[0]
235 obj = inst_map[api_name].m 174 if os.path.isdir(subpath):
236 assert not hasattr(obj, dep) 175 if not os.path.exists(os.path.join(subpath, '__init__.py')):
237 setattr(obj, dep, inst_map[dep]) 176 continue
238 to_remove.append(dep) 177 elif not subpath.endswith('.py') or subitem.startswith('__init__.py'):
239 did_something = True 178 continue
240 map(deps.remove, to_remove) 179
241 if not deps: 180 submod = _recursive_import(subpath, fullname)
242 to_pop.append(api_name) 181
243 did_something = True 182 if not hasattr(mod, subname):
244 map(dep_map.pop, to_pop) 183 setattr(mod, subname, submod)
245 assert did_something, 'Did nothing on this loop. %s' % dep_map 184 else:
185 prev = getattr(mod, subname)
186 assert submod is prev, (
187 'Conflicting modules: %s and %s' % (prev, mod))
188
189 return mod
246 190
247 191
248 def create_recipe_api(names, engine, test_data=DisabledTestData()): 192 def _patchup_module(name, submod):
249 return create_apis(MODULE_DIRS(), names, False, engine, test_data)[0] 193 """Finds framework related classes and functions in a |submod| and adds
194 them to |submod| as top level constants with well known names such as
195 API, CONFIG_CTX and TEST_API.
196
197 |submod| is a recipe module (akin to python package) with submodules such as
198 'api', 'config', 'test_api'. This function scans through dicts of that
199 submodules to find subclasses of RecipeApi, RecipeTestApi, etc.
200 """
201 submod.NAME = name
202 submod.UNIQUE_NAME = name # TODO(luqui): use a luci-config unique name
203 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod))
204 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None)
205
206 if hasattr(submod, 'config'):
207 for v in submod.config.__dict__.itervalues():
208 if isinstance(v, ConfigContext):
209 assert not submod.CONFIG_CTX, (
210 'More than one configuration context: %s, %s' %
211 (submod.config, submod.CONFIG_CTX))
212 submod.CONFIG_CTX = v
213 assert submod.CONFIG_CTX, 'Config file, but no config context?'
214
215 submod.API = getattr(submod, 'API', None)
216 for v in submod.api.__dict__.itervalues():
217 if inspect.isclass(v) and issubclass(v, RecipeApiPlain):
218 assert not submod.API, (
219 '%s has more than one Api subclass: %s, %s' % (name, v, submod.api))
220 submod.API = v
221 assert submod.API, 'Submodule has no api? %s' % (submod)
222
223 submod.TEST_API = getattr(submod, 'TEST_API', None)
224 if hasattr(submod, 'test_api'):
225 for v in submod.test_api.__dict__.itervalues():
226 if inspect.isclass(v) and issubclass(v, RecipeTestApi):
227 assert not submod.TEST_API, (
228 'More than one TestApi subclass: %s' % submod.api)
229 submod.TEST_API = v
230 assert submod.API, (
231 'Submodule has test_api.py but no TestApi subclass? %s'
232 % (submod)
233 )
250 234
251 235
252 def create_test_api(names): 236 class DependencyMapper(object):
253 # Test API should not use runtime engine or test_data, do not pass it. 237 """DependencyMapper topologically traverses the dependency DAG beginning at
254 return create_apis(MODULE_DIRS(), names, True, None, DisabledTestData())[1] 238 a module, executing a hook ("instantiator") for each module.
iannucci 2015/05/05 23:35:59 s/hook/callback instantiator(module, deps_dict)
luqui 2015/05/06 22:18:39 Done.
239
240 For example, if the dependency DAG looked like this:
241
242 A
243 / \
244 B C
245 \ /
246 D
247
248 (with D depending on B and C, etc.), DependencyMapper(f).instantiate(D) would
249 construct
250
iannucci 2015/05/05 23:35:59 f = instantiator
251 f_A = f(A, {})
252 f_B = f(B, { 'A': f_A })
253 f_C = f(C, { 'A': f_A })
254 f_D = f(D, { 'B': f_B, 'C': f_C })
255
256 finally returning f_D. instantiate can be called multiple times, which reuses
257 already-computed results.
258 """
259
260 def __init__(self, instantiator):
261 self._instantiator = instantiator
262 self._instances = {}
263
264 def instantiate(self, mod):
265 if mod in self._instances:
266 return self._instances[mod]
267 deps_dict = {}
268 for name, dep in mod.LOADED_DEPS.iteritems():
269 deps_dict[name] = self.instantiate(dep)
iannucci 2015/05/05 23:35:59 dict comprehension?
luqui 2015/05/06 22:18:39 Done.
270 self._instances[mod] = self._instantiator(mod, deps_dict)
271 return self._instances[mod]
255 272
256 273
257 @cached_unary 274 def create_recipe_api(toplevel_deps, engine, test_data=DisabledTestData()):
258 def load_recipe(recipe): 275 def instantiator(mod, deps):
276 mod_api = mod.API(module=mod, engine=engine,
277 test_data=test_data.get_module_test_data(mod.NAME))
iannucci 2015/05/05 23:35:59 NAME will need to be (eventually) a global name (p
luqui 2015/05/06 22:18:39 Acknowledged.
278 mod_api.test_api = (getattr(mod, 'TEST_API', None)
279 or RecipeTestApi)(module=mod)
280 for k, v in deps.iteritems():
281 setattr(mod_api.m, k, v)
282 setattr(mod_api.test_api.m, k, v.test_api)
283 return mod_api
284
285 mapper = DependencyMapper(instantiator)
286 api = RecipeApi(module=None, engine=engine,
287 test_data=test_data.get_module_test_data(None))
288 for k, v in toplevel_deps.iteritems():
289 setattr(api, k, mapper.instantiate(v))
290 return api
291
292
293 def create_test_api(toplevel_deps, loader):
294 def instantiator(mod, deps):
295 modapi = (getattr(mod, 'TEST_API', None) or RecipeTestApi)(module=mod)
296 for k,v in deps.iteritems():
297 setattr(modapi.m, k, v)
298 return modapi
299
300 mapper = DependencyMapper(instantiator)
301 api = RecipeTestApi(module=None)
302 for k,v in toplevel_deps.iteritems():
303 setattr(api, k, mapper.instantiate(v))
304 return api
305
306
307 def load_recipe(recipe, loader):
259 """Given name of a recipe, loads and returns it as RecipeScript instance. 308 """Given name of a recipe, loads and returns it as RecipeScript instance.
260 309
261 Args: 310 Args:
262 recipe (str): name of a recipe, can be in form '<module>:<recipe>'. 311 recipe (str): name of a recipe, can be in form '<module>:<recipe>'.
312 loader (RecipeModuleLoader): a loader to use
263 313
264 Returns: 314 Returns:
265 RecipeScript instance. 315 RecipeScript instance.
266 316
267 Raises: 317 Raises:
268 NoSuchRecipe: recipe is not found. 318 NoSuchRecipe: recipe is not found.
269 """ 319 """
270 # If the recipe is specified as "module:recipe", then it is an recipe 320 # If the recipe is specified as "module:recipe", then it is an recipe
271 # contained in a recipe_module as an example. Look for it in the modules 321 # contained in a recipe_module as an example. Look for it in the modules
272 # imported by load_recipe_modules instead of the normal search paths. 322 # imported by load_recipe_modules instead of the normal search paths.
273 if ':' in recipe: 323 if ':' in recipe:
274 module_name, example = recipe.split(':') 324 module_name, example = recipe.split(':')
275 assert example.endswith('example') 325 assert example.endswith('example')
276 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS()) 326 for module_dir in MODULE_DIRS():
277 try: 327 for subitem in os.listdir(module_dir):
278 script_module = getattr(getattr(RECIPE_MODULES, module_name), example) 328 if module_name == subitem:
279 return RecipeScript.from_module_object(script_module) 329 return RecipeScript.from_script_path(
280 except AttributeError: 330 os.path.join(module_dir, subitem, 'example.py'), loader)
281 raise NoSuchRecipe(recipe, 331 raise NoSuchRecipe(recipe,
282 'Recipe module %s does not have example %s defined' % 332 'Recipe example %s:%s does not exist' %
283 (module_name, example)) 333 (module_name, example))
284 else: 334 else:
285 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()): 335 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()):
286 if os.path.exists(recipe_path + '.py'): 336 if os.path.exists(recipe_path + '.py'):
287 return RecipeScript.from_script_path(recipe_path + '.py') 337 return RecipeScript.from_script_path(recipe_path + '.py', loader)
288 raise NoSuchRecipe(recipe) 338 raise NoSuchRecipe(recipe)
339
340
341 def loop_over_recipe_modules():
342 for path in MODULE_DIRS():
343 if os.path.isdir(path):
344 for item in os.listdir(path):
345 subpath = os.path.join(path, item)
346 if os.path.isdir(subpath):
347 yield subpath
289 348
290 349
291 def loop_over_recipes(): 350 def loop_over_recipes():
292 """Yields pairs (path to recipe, recipe name). 351 """Yields pairs (path to recipe, recipe name).
293 352
294 Enumerates real recipes in recipes/* as well as examples in recipe_modules/*. 353 Enumerates real recipes in recipes/* as well as examples in recipe_modules/*.
295 """ 354 """
296 for path in RECIPE_DIRS(): 355 for path in RECIPE_DIRS():
297 for recipe in scan_directory( 356 for recipe in scan_directory(
298 path, lambda f: f.endswith('.py') and f[0] != '_'): 357 path, lambda f: f.endswith('.py') and f[0] != '_'):
299 yield recipe, recipe[len(path)+1:-len('.py')] 358 yield recipe, recipe[len(path)+1:-len('.py')]
300 for path in MODULE_DIRS(): 359 for path in MODULE_DIRS():
301 for recipe in scan_directory( 360 for recipe in scan_directory(
302 path, lambda f: f.endswith('example.py')): 361 path, lambda f: f.endswith('example.py')):
303 module_name = os.path.dirname(recipe)[len(path)+1:] 362 module_name = os.path.dirname(recipe)[len(path)+1:]
304 yield recipe, '%s:example' % module_name 363 yield recipe, '%s:example' % module_name
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698