Index: third_party/recipe_engine/loader.py |
diff --git a/third_party/recipe_engine/loader.py b/third_party/recipe_engine/loader.py |
deleted file mode 100644 |
index eafbeb5d4f29b4187c134b979b36a3bfb0e3ff6b..0000000000000000000000000000000000000000 |
--- a/third_party/recipe_engine/loader.py |
+++ /dev/null |
@@ -1,460 +0,0 @@ |
-# Copyright 2013-2015 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-import contextlib |
-import imp |
-import inspect |
-import os |
-import sys |
- |
-from .config import ConfigContext |
-from .config_types import Path, ModuleBasePath, RECIPE_MODULE_PREFIX |
-from .recipe_api import RecipeApi, RecipeApiPlain, Property, UndefinedPropertyException |
-from .recipe_test_api import RecipeTestApi, DisabledTestData |
-from .util import scan_directory |
- |
- |
-class NoSuchRecipe(Exception): |
- """Raised by load_recipe is recipe is not found.""" |
- |
- |
-class RecipeScript(object): |
- """Holds dict of an evaluated recipe script.""" |
- |
- def __init__(self, recipe_dict): |
- recipe_dict.setdefault('PROPERTIES', {}) |
- # Let each property object know about the property name. |
- for name, value in recipe_dict['PROPERTIES'].items(): |
- value.name = name |
- |
- for k, v in recipe_dict.iteritems(): |
- setattr(self, k, v) |
- |
- @classmethod |
- def from_script_path(cls, script_path, universe): |
- """Evaluates a script and returns RecipeScript instance.""" |
- |
- script_vars = {} |
- script_vars['__file__'] = script_path |
- |
- with _preserve_path(): |
- execfile(script_path, script_vars) |
- |
- script_vars['LOADED_DEPS'] = universe.deps_from_mixed( |
- script_vars.get('DEPS', []), os.path.basename(script_path)) |
- return cls(script_vars) |
- |
- |
-class Dependency(object): |
- def load(self, universe): |
- raise NotImplementedError() |
- |
- @property |
- def local_name(self): |
- raise NotImplementedError() |
- |
- @property |
- def unique_name(self): |
- """A unique identifier for the module that this dependency refers to. |
- This must be generated without loading the module.""" |
- raise NotImplementedError() |
- |
- |
-class PathDependency(Dependency): |
- def __init__(self, path, local_name, universe, base_path=None): |
- self._path = _normalize_path(base_path, path) |
- self._local_name = local_name |
- |
- # We forbid modules from living outside our main paths to keep clients |
- # from going crazy before we have standardized recipe locations. |
- mod_dir = os.path.dirname(path) |
- assert mod_dir in universe.module_dirs, ( |
- 'Modules living outside of approved directories are forbidden: ' |
- '%s is not in %s' % (mod_dir, universe.module_dirs)) |
- |
- def load(self, universe): |
- return _load_recipe_module_module(self._path, universe) |
- |
- @property |
- def local_name(self): |
- return self._local_name |
- |
- @property |
- def unique_name(self): |
- return self._path |
- |
- |
-class NamedDependency(PathDependency): |
- def __init__(self, name, universe): |
- for path in universe.module_dirs: |
- mod_path = os.path.join(path, name) |
- if _is_recipe_module_dir(mod_path): |
- super(NamedDependency, self).__init__(mod_path, name, universe=universe) |
- return |
- raise NoSuchRecipe('Recipe module named %s does not exist' % name) |
- |
- |
-class RecipeUniverse(object): |
- def __init__(self, module_dirs, recipe_dirs): |
- self._loaded = {} |
- self._module_dirs = module_dirs[:] |
- self._recipe_dirs = recipe_dirs[:] |
- |
- @property |
- def module_dirs(self): |
- return self._module_dirs |
- |
- @property |
- def recipe_dirs(self): |
- return self._recipe_dirs |
- |
- def load(self, dep): |
- """Load a Dependency.""" |
- name = dep.unique_name |
- if name in self._loaded: |
- mod = self._loaded[name] |
- assert mod is not None, ( |
- 'Cyclic dependency when trying to load %s' % name) |
- return mod |
- else: |
- self._loaded[name] = None |
- mod = dep.load(self) |
- self._loaded[name] = mod |
- return mod |
- |
- def deps_from_names(self, deps): |
- """Load dependencies given a list simple module names (old style).""" |
- return { dep: self.load(NamedDependency(dep, universe=self)) |
- for dep in deps } |
- |
- def deps_from_paths(self, deps, base_path): |
- """Load dependencies given a dictionary of local names to module paths |
- (new style).""" |
- return { name: self.load(PathDependency(path, name, |
- universe=self, base_path=base_path)) |
- for name, path in deps.iteritems() } |
- |
- def deps_from_mixed(self, deps, base_path): |
- """Load dependencies given either a new style or old style deps spec.""" |
- if isinstance(deps, (list, tuple)): |
- return self.deps_from_names(deps) |
- elif isinstance(deps, dict): |
- return self.deps_from_paths(deps, base_path) |
- else: |
- raise ValueError('%s is not a valid or known deps structure' % deps) |
- |
- def load_recipe(self, recipe): |
- """Given name of a recipe, loads and returns it as RecipeScript instance. |
- |
- Args: |
- recipe (str): name of a recipe, can be in form '<module>:<recipe>'. |
- |
- Returns: |
- RecipeScript instance. |
- |
- Raises: |
- NoSuchRecipe: recipe is not found. |
- """ |
- # If the recipe is specified as "module:recipe", then it is an recipe |
- # contained in a recipe_module as an example. Look for it in the modules |
- # imported by load_recipe_modules instead of the normal search paths. |
- if ':' in recipe: |
- module_name, example = recipe.split(':') |
- assert example.endswith('example') |
- for module_dir in self.module_dirs: |
- for subitem in os.listdir(module_dir): |
- if module_name == subitem: |
- return RecipeScript.from_script_path( |
- os.path.join(module_dir, subitem, 'example.py'), self) |
- raise NoSuchRecipe(recipe, |
- 'Recipe example %s:%s does not exist' % |
- (module_name, example)) |
- else: |
- for recipe_path in (os.path.join(p, recipe) for p in self.recipe_dirs): |
- if os.path.exists(recipe_path + '.py'): |
- return RecipeScript.from_script_path(recipe_path + '.py', self) |
- raise NoSuchRecipe(recipe) |
- |
- def loop_over_recipe_modules(self): |
- for path in self.module_dirs: |
- if os.path.isdir(path): |
- for item in os.listdir(path): |
- subpath = os.path.join(path, item) |
- if _is_recipe_module_dir(subpath): |
- yield subpath |
- |
- def loop_over_recipes(self): |
- """Yields pairs (path to recipe, recipe name). |
- |
- Enumerates real recipes in recipes/* as well as examples in recipe_modules/*. |
- """ |
- for path in self.recipe_dirs: |
- for recipe in scan_directory( |
- path, lambda f: f.endswith('.py') and f[0] != '_'): |
- yield recipe, recipe[len(path)+1:-len('.py')] |
- for path in self.module_dirs: |
- for recipe in scan_directory( |
- path, lambda f: f.endswith('example.py')): |
- module_name = os.path.dirname(recipe)[len(path)+1:] |
- yield recipe, '%s:example' % module_name |
- |
- |
-def _is_recipe_module_dir(path): |
- return (os.path.isdir(path) and |
- os.path.isfile(os.path.join(path, '__init__.py'))) |
- |
- |
-@contextlib.contextmanager |
-def _preserve_path(): |
- old_path = sys.path[:] |
- try: |
- yield |
- finally: |
- sys.path = old_path |
- |
- |
-def _normalize_path(base_path, path): |
- if base_path is None or os.path.isabs(path): |
- return os.path.realpath(path) |
- else: |
- return os.path.realpath(os.path.join(base_path, path)) |
- |
- |
-def _find_and_load_module(fullname, modname, path): |
- imp.acquire_lock() |
- try: |
- if fullname not in sys.modules: |
- fil = None |
- try: |
- fil, pathname, descr = imp.find_module(modname, |
- [os.path.dirname(path)]) |
- imp.load_module(fullname, fil, pathname, descr) |
- finally: |
- if fil: |
- fil.close() |
- return sys.modules[fullname] |
- finally: |
- imp.release_lock() |
- |
- |
-def _load_recipe_module_module(path, universe): |
- modname = os.path.splitext(os.path.basename(path))[0] |
- fullname = '%s.%s' % (RECIPE_MODULE_PREFIX, modname) |
- mod = _find_and_load_module(fullname, modname, path) |
- |
- # This actually loads the dependencies. |
- mod.LOADED_DEPS = universe.deps_from_mixed( |
- getattr(mod, 'DEPS', []), os.path.basename(path)) |
- |
- # Prevent any modules that mess with sys.path from leaking. |
- with _preserve_path(): |
- # TODO(luqui): Remove this hack once configs are cleaned. |
- sys.modules['%s.DEPS' % fullname] = mod.LOADED_DEPS |
- _recursive_import(path, RECIPE_MODULE_PREFIX) |
- _patchup_module(modname, mod) |
- |
- return mod |
- |
- |
-def _recursive_import(path, prefix): |
- modname = os.path.splitext(os.path.basename(path))[0] |
- fullname = '%s.%s' % (prefix, modname) |
- mod = _find_and_load_module(fullname, modname, path) |
- if not os.path.isdir(path): |
- return mod |
- |
- for subitem in os.listdir(path): |
- subpath = os.path.join(path, subitem) |
- subname = os.path.splitext(subitem)[0] |
- if os.path.isdir(subpath): |
- if not os.path.exists(os.path.join(subpath, '__init__.py')): |
- continue |
- elif not subpath.endswith('.py') or subitem.startswith('__init__.py'): |
- continue |
- |
- submod = _recursive_import(subpath, fullname) |
- |
- if not hasattr(mod, subname): |
- setattr(mod, subname, submod) |
- else: |
- prev = getattr(mod, subname) |
- assert submod is prev, ( |
- 'Conflicting modules: %s and %s' % (prev, mod)) |
- |
- return mod |
- |
- |
-def _patchup_module(name, submod): |
- """Finds framework related classes and functions in a |submod| and adds |
- them to |submod| as top level constants with well known names such as |
- API, CONFIG_CTX, TEST_API, and PROPERTIES. |
- |
- |submod| is a recipe module (akin to python package) with submodules such as |
- 'api', 'config', 'test_api'. This function scans through dicts of that |
- submodules to find subclasses of RecipeApi, RecipeTestApi, etc. |
- """ |
- submod.NAME = name |
- submod.UNIQUE_NAME = name # TODO(luqui): use a luci-config unique name |
- submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod)) |
- submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) |
- |
- if hasattr(submod, 'config'): |
- for v in submod.config.__dict__.itervalues(): |
- if isinstance(v, ConfigContext): |
- assert not submod.CONFIG_CTX, ( |
- 'More than one configuration context: %s, %s' % |
- (submod.config, submod.CONFIG_CTX)) |
- submod.CONFIG_CTX = v |
- assert submod.CONFIG_CTX, 'Config file, but no config context?' |
- |
- submod.API = getattr(submod, 'API', None) |
- for v in submod.api.__dict__.itervalues(): |
- if inspect.isclass(v) and issubclass(v, RecipeApiPlain): |
- assert not submod.API, ( |
- '%s has more than one Api subclass: %s, %s' % (name, v, submod.api)) |
- submod.API = v |
- assert submod.API, 'Submodule has no api? %s' % (submod) |
- |
- submod.TEST_API = getattr(submod, 'TEST_API', None) |
- if hasattr(submod, 'test_api'): |
- for v in submod.test_api.__dict__.itervalues(): |
- if inspect.isclass(v) and issubclass(v, RecipeTestApi): |
- assert not submod.TEST_API, ( |
- 'More than one TestApi subclass: %s' % submod.api) |
- submod.TEST_API = v |
- assert submod.API, ( |
- 'Submodule has test_api.py but no TestApi subclass? %s' |
- % (submod) |
- ) |
- |
- submod.PROPERTIES = getattr(submod, 'PROPERTIES', {}) |
- # Let each property object know about the property name. |
- for name, value in submod.PROPERTIES.items(): |
- value.name = name |
- |
- |
-class DependencyMapper(object): |
- """DependencyMapper topologically traverses the dependency DAG beginning at |
- a module, executing a callback ("instantiator") for each module. |
- |
- For example, if the dependency DAG looked like this: |
- |
- A |
- / \ |
- B C |
- \ / |
- D |
- |
- (with D depending on B and C, etc.), DependencyMapper(f).instantiate(D) would |
- construct |
- |
- f_A = f(A, {}) |
- f_B = f(B, { 'A': f_A }) |
- f_C = f(C, { 'A': f_A }) |
- f_D = f(D, { 'B': f_B, 'C': f_C }) |
- |
- finally returning f_D. instantiate can be called multiple times, which reuses |
- already-computed results. |
- """ |
- |
- def __init__(self, instantiator): |
- self._instantiator = instantiator |
- self._instances = {} |
- |
- def instantiate(self, mod): |
- if mod in self._instances: |
- return self._instances[mod] |
- deps_dict = { name: self.instantiate(dep) |
- for name, dep in mod.LOADED_DEPS.iteritems() } |
- self._instances[mod] = self._instantiator(mod, deps_dict) |
- return self._instances[mod] |
- |
-def invoke_with_properties(callable_obj, all_props, prop_defs, |
- **additional_args): |
- """ |
- Invokes callable with filtered, type-checked properties. |
- |
- Args: |
- callable_obj: The function to call, or class to instantiate. |
- This supports passing in either RunSteps, or a recipe module, |
- which is a class. |
- all_props: A dictionary containing all the properties |
- currently defined in the system. |
- prop_defs: A dictionary of name to property definitions for this callable. |
- additional_args: kwargs to pass through to the callable. |
- Note that the names of the arguments can correspond to |
- positional arguments as well. |
- |
- Returns: |
- The result of calling callable with the filtered properties |
- and additional arguments. |
- """ |
- # To detect when they didn't specify a property that they have as a |
- # function argument, list the arguments, through inspection, |
- # and then comparing this list to the provided properties. We use a list |
- # instead of a dict because getargspec returns a list which we would have to |
- # convert to a dictionary, and the benefit of the dictionary is pretty small. |
- props = [] |
- if inspect.isclass(callable_obj): |
- arg_names = inspect.getargspec(callable_obj.__init__).args |
- |
- arg_names.pop(0) |
- else: |
- arg_names = inspect.getargspec(callable_obj).args |
- |
- for arg in arg_names: |
- if arg in additional_args: |
- props.append(additional_args.pop(arg)) |
- continue |
- |
- if arg not in prop_defs: |
- raise UndefinedPropertyException( |
- "Missing property definition for '{}'.".format(arg)) |
- |
- props.append(prop_defs[arg].interpret(all_props.get( |
- arg, Property.sentinel))) |
- |
- return callable_obj(*props, **additional_args) |
- |
-def create_recipe_api(toplevel_deps, engine, test_data=DisabledTestData()): |
- def instantiator(mod, deps): |
- kwargs = { |
- 'module': mod, |
- 'engine': engine, |
- # TODO(luqui): test_data will need to use canonical unique names. |
- 'test_data': test_data.get_module_test_data(mod.NAME) |
- } |
- prop_defs = mod.PROPERTIES |
- mod_api = invoke_with_properties( |
- mod.API, engine.properties, prop_defs, **kwargs) |
- mod_api.test_api = (getattr(mod, 'TEST_API', None) |
- or RecipeTestApi)(module=mod) |
- for k, v in deps.iteritems(): |
- setattr(mod_api.m, k, v) |
- setattr(mod_api.test_api.m, k, v.test_api) |
- return mod_api |
- |
- mapper = DependencyMapper(instantiator) |
- api = RecipeApi(module=None, engine=engine, |
- test_data=test_data.get_module_test_data(None)) |
- for k, v in toplevel_deps.iteritems(): |
- setattr(api, k, mapper.instantiate(v)) |
- return api |
- |
- |
-def create_test_api(toplevel_deps, universe): |
- def instantiator(mod, deps): |
- modapi = (getattr(mod, 'TEST_API', None) or RecipeTestApi)(module=mod) |
- for k,v in deps.iteritems(): |
- setattr(modapi.m, k, v) |
- return modapi |
- |
- mapper = DependencyMapper(instantiator) |
- api = RecipeTestApi(module=None) |
- for k,v in toplevel_deps.iteritems(): |
- setattr(api, k, mapper.instantiate(v)) |
- return api |
- |
- |
- |