Chromium Code Reviews| OLD | NEW |
|---|---|
| 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 |
| OLD | NEW |