Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import collections | 5 import collections |
| 6 import contextlib | 6 import contextlib |
| 7 import imp | 7 import imp |
| 8 import inspect | 8 import inspect |
| 9 import os | 9 import os |
| 10 import sys | 10 import sys |
| 11 | 11 |
| 12 from . import env | 12 from . import env |
| 13 | 13 |
| 14 from .config import ConfigContext, ConfigGroupSchema | 14 from .config import ConfigContext, ConfigGroupSchema |
| 15 from .config_types import Path, ModuleBasePath, PackageRepoBasePath | 15 from .config_types import Path, ModuleBasePath, PackageRepoBasePath |
| 16 from .config_types import RecipeScriptBasePath | |
| 16 from .config_types import RECIPE_MODULE_PREFIX | 17 from .config_types import RECIPE_MODULE_PREFIX |
| 17 from .recipe_api import RecipeApi, RecipeApiPlain, RecipeScriptApi | 18 from .recipe_api import RecipeApi, RecipeApiPlain, RecipeScriptApi |
| 18 from .recipe_api import _UnresolvedRequirement | 19 from .recipe_api import _UnresolvedRequirement |
| 19 from .recipe_api import Property, BoundProperty | 20 from .recipe_api import Property, BoundProperty |
| 20 from .recipe_api import UndefinedPropertyException, PROPERTY_SENTINEL | 21 from .recipe_api import UndefinedPropertyException, PROPERTY_SENTINEL |
| 21 from .recipe_test_api import RecipeTestApi, DisabledTestData | 22 from .recipe_test_api import RecipeTestApi, DisabledTestData |
| 22 from .util import scan_directory | |
| 23 | 23 |
| 24 | 24 |
| 25 class LoaderError(Exception): | 25 class LoaderError(Exception): |
| 26 """Raised when something goes wrong loading recipes or modules.""" | 26 """Raised when something goes wrong loading recipes or modules.""" |
| 27 | 27 |
| 28 | 28 |
| 29 class NoSuchRecipe(LoaderError): | 29 class NoSuchRecipe(LoaderError): |
| 30 """Raised by load_recipe is recipe is not found.""" | 30 """Raised by load_recipe is recipe is not found.""" |
| 31 def __init__(self, recipe): | 31 def __init__(self, recipe): |
| 32 super(NoSuchRecipe, self).__init__('No such recipe: %s' % recipe) | 32 super(NoSuchRecipe, self).__init__('No such recipe: %s' % recipe) |
| 33 | 33 |
| 34 | 34 |
| 35 class RecipeScript(object): | 35 class RecipeScript(object): |
| 36 """Holds dict of an evaluated recipe script.""" | 36 """Holds dict of an evaluated recipe script.""" |
| 37 | 37 |
| 38 def __init__(self, recipe_globals, name): | 38 def __init__(self, recipe_globals, path): |
| 39 self.name = name | 39 self.path = path |
| 40 self._recipe_globals = recipe_globals | 40 self._recipe_globals = recipe_globals |
| 41 | 41 |
| 42 self.run_steps, self.gen_tests = [ | 42 self.run_steps, self.gen_tests = [ |
| 43 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] | 43 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] |
| 44 | 44 |
| 45 # Let each property object know about the property name. | 45 # Let each property object know about the property name. |
| 46 recipe_globals['PROPERTIES'] = { | 46 recipe_globals['PROPERTIES'] = { |
| 47 name: value.bind(name, BoundProperty.RECIPE_PROPERTY, name) | 47 name: value.bind(name, BoundProperty.RECIPE_PROPERTY, name) |
| 48 for name, value in recipe_globals.get('PROPERTIES', {}).items()} | 48 for name, value in recipe_globals.get('PROPERTIES', {}).items()} |
| 49 | 49 |
| 50 return_schema = recipe_globals.get('RETURN_SCHEMA') | 50 return_schema = recipe_globals.get('RETURN_SCHEMA') |
| 51 if return_schema and not isinstance(return_schema, ConfigGroupSchema): | 51 if return_schema and not isinstance(return_schema, ConfigGroupSchema): |
| 52 raise ValueError('Invalid RETURN_SCHEMA; must be an instance of ' | 52 raise ValueError('Invalid RETURN_SCHEMA; must be an instance of ' |
| 53 'ConfigGroupSchema') | 53 'ConfigGroupSchema') |
| 54 | 54 |
| 55 @property | 55 @property |
| 56 def name(self): | |
| 57 # 'a/b/c/my_name.py' -> my_name | |
| 58 return os.path.splitext(os.path.basename(self.path))[0] | |
| 59 | |
| 60 @property | |
| 56 def globals(self): | 61 def globals(self): |
| 57 return self._recipe_globals | 62 return self._recipe_globals |
| 58 | 63 |
| 59 @property | 64 @property |
| 60 def PROPERTIES(self): | 65 def PROPERTIES(self): |
| 61 return self._recipe_globals['PROPERTIES'] | 66 return self._recipe_globals['PROPERTIES'] |
| 62 | 67 |
| 63 @property | 68 @property |
| 64 def LOADED_DEPS(self): | 69 def LOADED_DEPS(self): |
| 65 return self._recipe_globals['LOADED_DEPS'] | 70 return self._recipe_globals['LOADED_DEPS'] |
| (...skipping 23 matching lines...) Expand all Loading... | |
| 89 | 94 |
| 90 recipe_globals = {} | 95 recipe_globals = {} |
| 91 recipe_globals['__file__'] = script_path | 96 recipe_globals['__file__'] = script_path |
| 92 | 97 |
| 93 with env.temp_sys_path(): | 98 with env.temp_sys_path(): |
| 94 execfile(script_path, recipe_globals) | 99 execfile(script_path, recipe_globals) |
| 95 | 100 |
| 96 recipe_globals['LOADED_DEPS'] = universe_view.deps_from_spec( | 101 recipe_globals['LOADED_DEPS'] = universe_view.deps_from_spec( |
| 97 recipe_globals.get('DEPS', [])) | 102 recipe_globals.get('DEPS', [])) |
| 98 | 103 |
| 99 # 'a/b/c/my_name.py' -> my_name | 104 return cls(recipe_globals, script_path) |
| 100 name = os.path.basename(script_path).split('.')[0] | |
| 101 return cls(recipe_globals, name) | |
| 102 | 105 |
| 103 | 106 |
| 104 class RecipeUniverse(object): | 107 class RecipeUniverse(object): |
| 105 def __init__(self, package_deps, config_file): | 108 def __init__(self, package_deps, config_file): |
| 106 self._loaded = {} | 109 self._loaded = {} |
| 107 self._package_deps = package_deps | 110 self._package_deps = package_deps |
| 108 self._config_file = config_file | 111 self._config_file = config_file |
| 109 | 112 |
| 110 @property | 113 @property |
| 111 def module_dirs(self): | 114 def module_dirs(self): |
| (...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 270 def recipe_dirs(self): | 273 def recipe_dirs(self): |
| 271 for recipe_dir in self.package.recipe_dirs: | 274 for recipe_dir in self.package.recipe_dirs: |
| 272 yield recipe_dir | 275 yield recipe_dir |
| 273 | 276 |
| 274 def loop_over_recipes(self): | 277 def loop_over_recipes(self): |
| 275 """Yields pairs (path to recipe, recipe name). | 278 """Yields pairs (path to recipe, recipe name). |
| 276 | 279 |
| 277 Enumerates real recipes in recipes/*, as well as examples in | 280 Enumerates real recipes in recipes/*, as well as examples in |
| 278 recipe_modules/*. | 281 recipe_modules/*. |
| 279 """ | 282 """ |
| 283 def scan_directory(path, predicate): | |
|
iannucci
2016/11/19 00:15:32
this was only used inside loop_over_recipes, so I
| |
| 284 for root, dirs, files in os.walk(path): | |
| 285 dirs[:] = [x for x in dirs | |
| 286 if not x.endswith(('.expected', '.resources'))] | |
| 287 for file_name in (f for f in files if predicate(f)): | |
| 288 file_path = os.path.join(root, file_name) | |
| 289 yield file_path | |
| 290 | |
| 280 for path in self.package.recipe_dirs: | 291 for path in self.package.recipe_dirs: |
| 281 for recipe in scan_directory( | 292 for recipe in scan_directory( |
| 282 path, lambda f: f.endswith('.py') and f[0] != '_'): | 293 path, lambda f: f.endswith('.py') and f[0] != '_'): |
| 283 yield recipe, recipe[len(path)+1:-len('.py')] | 294 yield recipe, recipe[len(path)+1:-len('.py')] |
| 284 for path in self.package.module_dirs: | 295 for path in self.package.module_dirs: |
| 285 for recipe in scan_directory( | 296 for recipe in scan_directory( |
| 286 path, lambda f: f.endswith('example.py')): | 297 path, lambda f: f.endswith('example.py')): |
| 287 module_name = os.path.dirname(recipe)[len(path)+1:] | 298 module_name = os.path.dirname(recipe)[len(path)+1:] |
| 288 yield recipe, '%s:example' % module_name | 299 yield recipe, '%s:example' % module_name |
| 289 | 300 |
| (...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 377 API, CONFIG_CTX, TEST_API, and PROPERTIES. | 388 API, CONFIG_CTX, TEST_API, and PROPERTIES. |
| 378 | 389 |
| 379 |submod| is a recipe module (akin to python package) with submodules such as | 390 |submod| is a recipe module (akin to python package) with submodules such as |
| 380 'api', 'config', 'test_api'. This function scans through dicts of that | 391 'api', 'config', 'test_api'. This function scans through dicts of that |
| 381 submodules to find subclasses of RecipeApi, RecipeTestApi, etc. | 392 submodules to find subclasses of RecipeApi, RecipeTestApi, etc. |
| 382 """ | 393 """ |
| 383 fullname = '%s/%s' % (universe_view.package.name, name) | 394 fullname = '%s/%s' % (universe_view.package.name, name) |
| 384 submod.NAME = name | 395 submod.NAME = name |
| 385 submod.UNIQUE_NAME = fullname | 396 submod.UNIQUE_NAME = fullname |
| 386 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod)) | 397 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod)) |
| 398 submod.RESOURCE_DIRECTORY = submod.MODULE_DIRECTORY.join('resources') | |
| 387 submod.PACKAGE_REPO_ROOT = Path(PackageRepoBasePath(universe_view.package)) | 399 submod.PACKAGE_REPO_ROOT = Path(PackageRepoBasePath(universe_view.package)) |
| 388 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) | 400 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) |
| 389 | 401 |
| 390 if hasattr(submod, 'config'): | 402 if hasattr(submod, 'config'): |
| 391 for v in submod.config.__dict__.itervalues(): | 403 for v in submod.config.__dict__.itervalues(): |
| 392 if isinstance(v, ConfigContext): | 404 if isinstance(v, ConfigContext): |
| 393 assert not submod.CONFIG_CTX, ( | 405 assert not submod.CONFIG_CTX, ( |
| 394 'More than one configuration context: %s, %s' % | 406 'More than one configuration context: %s, %s' % |
| 395 (submod.config, submod.CONFIG_CTX)) | 407 (submod.config, submod.CONFIG_CTX)) |
| 396 submod.CONFIG_CTX = v | 408 submod.CONFIG_CTX = v |
| (...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 537 if inspect.isclass(callable_obj): | 549 if inspect.isclass(callable_obj): |
| 538 arg_names = inspect.getargspec(callable_obj.__init__).args | 550 arg_names = inspect.getargspec(callable_obj.__init__).args |
| 539 | 551 |
| 540 arg_names.pop(0) | 552 arg_names.pop(0) |
| 541 else: | 553 else: |
| 542 arg_names = inspect.getargspec(callable_obj).args | 554 arg_names = inspect.getargspec(callable_obj).args |
| 543 return _invoke_with_properties(callable_obj, all_props, prop_defs, arg_names, | 555 return _invoke_with_properties(callable_obj, all_props, prop_defs, arg_names, |
| 544 **additional_args) | 556 **additional_args) |
| 545 | 557 |
| 546 | 558 |
| 547 def create_recipe_api(toplevel_deps, engine, test_data=DisabledTestData()): | 559 def create_recipe_api(toplevel_package, toplevel_deps, recipe_script_path, |
| 560 engine, test_data=DisabledTestData()): | |
| 548 def instantiator(mod, deps): | 561 def instantiator(mod, deps): |
| 549 kwargs = { | 562 kwargs = { |
| 550 'module': mod, | 563 'module': mod, |
| 551 # TODO(luqui): test_data will need to use canonical unique names. | 564 # TODO(luqui): test_data will need to use canonical unique names. |
| 552 'test_data': test_data.get_module_test_data(mod.NAME) | 565 'test_data': test_data.get_module_test_data(mod.NAME) |
| 553 } | 566 } |
| 554 prop_defs = mod.PROPERTIES | 567 prop_defs = mod.PROPERTIES |
| 555 mod_api = invoke_with_properties( | 568 mod_api = invoke_with_properties( |
| 556 mod.API, engine.properties, prop_defs, **kwargs) | 569 mod.API, engine.properties, prop_defs, **kwargs) |
| 557 mod_api.test_api = (getattr(mod, 'TEST_API', None) | 570 mod_api.test_api = (getattr(mod, 'TEST_API', None) |
| 558 or RecipeTestApi)(module=mod) | 571 or RecipeTestApi)(module=mod) |
| 559 for k, v in deps.iteritems(): | 572 for k, v in deps.iteritems(): |
| 560 setattr(mod_api.m, k, v) | 573 setattr(mod_api.m, k, v) |
| 561 setattr(mod_api.test_api.m, k, v.test_api) | 574 setattr(mod_api.test_api.m, k, v.test_api) |
| 562 | 575 |
| 563 # Replace class-level Requirements placeholders in the recipe API with | 576 # Replace class-level Requirements placeholders in the recipe API with |
| 564 # their instance-level real values. | 577 # their instance-level real values. |
| 565 map(lambda (k, v): setattr(mod_api, k, _resolve_requirement(v, engine)), | 578 map(lambda (k, v): setattr(mod_api, k, _resolve_requirement(v, engine)), |
| 566 ((k, v) for k, v in type(mod_api).__dict__.iteritems() | 579 ((k, v) for k, v in type(mod_api).__dict__.iteritems() |
| 567 if isinstance(v, _UnresolvedRequirement))) | 580 if isinstance(v, _UnresolvedRequirement))) |
| 568 | 581 |
| 569 mod_api.initialize() | 582 mod_api.initialize() |
| 570 return mod_api | 583 return mod_api |
| 571 | 584 |
| 572 mapper = DependencyMapper(instantiator) | 585 mapper = DependencyMapper(instantiator) |
| 573 api = RecipeScriptApi(module=None, engine=engine, | 586 # Provide a fake module to the ScriptApi so that recipes can use: |
| 587 # * .name | |
| 588 # * .resource | |
| 589 # * .package_repo_resource | |
| 590 # This is obviously a hack, however it homogenizes the api and removes the | |
| 591 # need for some ugly workarounds in user code. A better way to do this would | |
| 592 # be to migrate all recipes to be members of modules. | |
| 593 name = os.path.join( | |
| 594 toplevel_package.name, | |
| 595 os.path.relpath( | |
| 596 os.path.splitext(recipe_script_path)[0], | |
| 597 toplevel_package.recipe_dirs[0])) | |
| 598 fakeModule = collections.namedtuple( | |
| 599 "fakeModule", "PACKAGE_REPO_ROOT NAME RESOURCE_DIRECTORY")( | |
| 600 Path(PackageRepoBasePath(toplevel_package)), | |
| 601 name, | |
| 602 Path(RecipeScriptBasePath( | |
| 603 name, os.path.splitext(recipe_script_path)[0]+".resources"))) | |
| 604 api = RecipeScriptApi(module=fakeModule, engine=engine, | |
| 574 test_data=test_data.get_module_test_data(None)) | 605 test_data=test_data.get_module_test_data(None)) |
| 575 for k, v in toplevel_deps.iteritems(): | 606 for k, v in toplevel_deps.iteritems(): |
| 576 setattr(api, k, mapper.instantiate(v)) | 607 setattr(api, k, mapper.instantiate(v)) |
| 577 return api | 608 return api |
| 578 | 609 |
| 579 | 610 |
| 580 def create_test_api(toplevel_deps, universe): | 611 def create_test_api(toplevel_deps, universe): |
| 581 def instantiator(mod, deps): | 612 def instantiator(mod, deps): |
| 582 modapi = (getattr(mod, 'TEST_API', None) or RecipeTestApi)(module=mod) | 613 modapi = (getattr(mod, 'TEST_API', None) or RecipeTestApi)(module=mod) |
| 583 for k,v in deps.iteritems(): | 614 for k,v in deps.iteritems(): |
| 584 setattr(modapi.m, k, v) | 615 setattr(modapi.m, k, v) |
| 585 return modapi | 616 return modapi |
| 586 | 617 |
| 587 mapper = DependencyMapper(instantiator) | 618 mapper = DependencyMapper(instantiator) |
| 588 api = RecipeTestApi(module=None) | 619 api = RecipeTestApi(module=None) |
| 589 for k,v in toplevel_deps.iteritems(): | 620 for k,v in toplevel_deps.iteritems(): |
| 590 setattr(api, k, mapper.instantiate(v)) | 621 setattr(api, k, mapper.instantiate(v)) |
| 591 return api | 622 return api |
| 592 | 623 |
| 593 | 624 |
| 594 def _resolve_requirement(req, engine): | 625 def _resolve_requirement(req, engine): |
| 595 if req._typ == 'client': | 626 if req._typ == 'client': |
| 596 return engine._get_client(req._name) | 627 return engine._get_client(req._name) |
| 597 else: | 628 else: |
| 598 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) | 629 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) |
| OLD | NEW |