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 from .util import scan_directory |
| 23 | 24 |
| 24 | 25 |
| 25 class LoaderError(Exception): | 26 class LoaderError(Exception): |
| 26 """Raised when something goes wrong loading recipes or modules.""" | 27 """Raised when something goes wrong loading recipes or modules.""" |
| 27 | 28 |
| 28 | 29 |
| 29 class NoSuchRecipe(LoaderError): | 30 class NoSuchRecipe(LoaderError): |
| 30 """Raised by load_recipe is recipe is not found.""" | 31 """Raised by load_recipe is recipe is not found.""" |
| 31 def __init__(self, recipe): | 32 def __init__(self, recipe): |
| 32 super(NoSuchRecipe, self).__init__('No such recipe: %s' % recipe) | 33 super(NoSuchRecipe, self).__init__('No such recipe: %s' % recipe) |
| 33 | 34 |
| 34 | 35 |
| 35 class RecipeScript(object): | 36 class RecipeScript(object): |
| 36 """Holds dict of an evaluated recipe script.""" | 37 """Holds dict of an evaluated recipe script.""" |
| 37 | 38 |
| 38 def __init__(self, recipe_globals, name): | 39 def __init__(self, recipe_globals, path): |
| 39 self.name = name | 40 self.path = path |
| 40 self._recipe_globals = recipe_globals | 41 self._recipe_globals = recipe_globals |
| 41 | 42 |
| 42 self.run_steps, self.gen_tests = [ | 43 self.run_steps, self.gen_tests = [ |
| 43 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] | 44 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] |
| 44 | 45 |
| 45 # Let each property object know about the property name. | 46 # Let each property object know about the property name. |
| 46 recipe_globals['PROPERTIES'] = { | 47 recipe_globals['PROPERTIES'] = { |
| 47 name: value.bind(name, BoundProperty.RECIPE_PROPERTY, name) | 48 name: value.bind(name, BoundProperty.RECIPE_PROPERTY, name) |
| 48 for name, value in recipe_globals.get('PROPERTIES', {}).items()} | 49 for name, value in recipe_globals.get('PROPERTIES', {}).items()} |
| 49 | 50 |
| 50 return_schema = recipe_globals.get('RETURN_SCHEMA') | 51 return_schema = recipe_globals.get('RETURN_SCHEMA') |
| 51 if return_schema and not isinstance(return_schema, ConfigGroupSchema): | 52 if return_schema and not isinstance(return_schema, ConfigGroupSchema): |
| 52 raise ValueError('Invalid RETURN_SCHEMA; must be an instance of ' | 53 raise ValueError('Invalid RETURN_SCHEMA; must be an instance of ' |
| 53 'ConfigGroupSchema') | 54 'ConfigGroupSchema') |
| 54 | 55 |
| 55 @property | 56 @property |
| 57 def name(self): | |
| 58 # 'a/b/c/my_name.py' -> my_name | |
| 59 return os.path.basename(self.path).split('.')[0] | |
|
nodir
2016/11/18 22:42:25
os.path.splitext?
iannucci
2016/11/19 00:00:57
Done.
| |
| 60 | |
| 61 @property | |
| 56 def globals(self): | 62 def globals(self): |
| 57 return self._recipe_globals | 63 return self._recipe_globals |
| 58 | 64 |
| 59 @property | 65 @property |
| 60 def PROPERTIES(self): | 66 def PROPERTIES(self): |
| 61 return self._recipe_globals['PROPERTIES'] | 67 return self._recipe_globals['PROPERTIES'] |
| 62 | 68 |
| 63 @property | 69 @property |
| 64 def LOADED_DEPS(self): | 70 def LOADED_DEPS(self): |
| 65 return self._recipe_globals['LOADED_DEPS'] | 71 return self._recipe_globals['LOADED_DEPS'] |
| (...skipping 23 matching lines...) Expand all Loading... | |
| 89 | 95 |
| 90 recipe_globals = {} | 96 recipe_globals = {} |
| 91 recipe_globals['__file__'] = script_path | 97 recipe_globals['__file__'] = script_path |
| 92 | 98 |
| 93 with env.temp_sys_path(): | 99 with env.temp_sys_path(): |
| 94 execfile(script_path, recipe_globals) | 100 execfile(script_path, recipe_globals) |
| 95 | 101 |
| 96 recipe_globals['LOADED_DEPS'] = universe_view.deps_from_spec( | 102 recipe_globals['LOADED_DEPS'] = universe_view.deps_from_spec( |
| 97 recipe_globals.get('DEPS', [])) | 103 recipe_globals.get('DEPS', [])) |
| 98 | 104 |
| 99 # 'a/b/c/my_name.py' -> my_name | 105 return cls(recipe_globals, script_path) |
| 100 name = os.path.basename(script_path).split('.')[0] | |
| 101 return cls(recipe_globals, name) | |
| 102 | 106 |
| 103 | 107 |
| 104 class RecipeUniverse(object): | 108 class RecipeUniverse(object): |
| 105 def __init__(self, package_deps, config_file): | 109 def __init__(self, package_deps, config_file): |
| 106 self._loaded = {} | 110 self._loaded = {} |
| 107 self._package_deps = package_deps | 111 self._package_deps = package_deps |
| 108 self._config_file = config_file | 112 self._config_file = config_file |
| 109 | 113 |
| 110 @property | 114 @property |
| 111 def module_dirs(self): | 115 def module_dirs(self): |
| (...skipping 265 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 377 API, CONFIG_CTX, TEST_API, and PROPERTIES. | 381 API, CONFIG_CTX, TEST_API, and PROPERTIES. |
| 378 | 382 |
| 379 |submod| is a recipe module (akin to python package) with submodules such as | 383 |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 | 384 'api', 'config', 'test_api'. This function scans through dicts of that |
| 381 submodules to find subclasses of RecipeApi, RecipeTestApi, etc. | 385 submodules to find subclasses of RecipeApi, RecipeTestApi, etc. |
| 382 """ | 386 """ |
| 383 fullname = '%s/%s' % (universe_view.package.name, name) | 387 fullname = '%s/%s' % (universe_view.package.name, name) |
| 384 submod.NAME = name | 388 submod.NAME = name |
| 385 submod.UNIQUE_NAME = fullname | 389 submod.UNIQUE_NAME = fullname |
| 386 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod)) | 390 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod)) |
| 391 submod.RESOURCE_DIRECTORY = submod.MODULE_DIRECTORY.join('resources') | |
| 387 submod.PACKAGE_REPO_ROOT = Path(PackageRepoBasePath(universe_view.package)) | 392 submod.PACKAGE_REPO_ROOT = Path(PackageRepoBasePath(universe_view.package)) |
| 388 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) | 393 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) |
| 389 | 394 |
| 390 if hasattr(submod, 'config'): | 395 if hasattr(submod, 'config'): |
| 391 for v in submod.config.__dict__.itervalues(): | 396 for v in submod.config.__dict__.itervalues(): |
| 392 if isinstance(v, ConfigContext): | 397 if isinstance(v, ConfigContext): |
| 393 assert not submod.CONFIG_CTX, ( | 398 assert not submod.CONFIG_CTX, ( |
| 394 'More than one configuration context: %s, %s' % | 399 'More than one configuration context: %s, %s' % |
| 395 (submod.config, submod.CONFIG_CTX)) | 400 (submod.config, submod.CONFIG_CTX)) |
| 396 submod.CONFIG_CTX = v | 401 submod.CONFIG_CTX = v |
| (...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 537 if inspect.isclass(callable_obj): | 542 if inspect.isclass(callable_obj): |
| 538 arg_names = inspect.getargspec(callable_obj.__init__).args | 543 arg_names = inspect.getargspec(callable_obj.__init__).args |
| 539 | 544 |
| 540 arg_names.pop(0) | 545 arg_names.pop(0) |
| 541 else: | 546 else: |
| 542 arg_names = inspect.getargspec(callable_obj).args | 547 arg_names = inspect.getargspec(callable_obj).args |
| 543 return _invoke_with_properties(callable_obj, all_props, prop_defs, arg_names, | 548 return _invoke_with_properties(callable_obj, all_props, prop_defs, arg_names, |
| 544 **additional_args) | 549 **additional_args) |
| 545 | 550 |
| 546 | 551 |
| 547 def create_recipe_api(toplevel_deps, engine, test_data=DisabledTestData()): | 552 def create_recipe_api(toplevel_package, toplevel_deps, recipe_script_path, |
| 553 engine, test_data=DisabledTestData()): | |
| 548 def instantiator(mod, deps): | 554 def instantiator(mod, deps): |
| 549 kwargs = { | 555 kwargs = { |
| 550 'module': mod, | 556 'module': mod, |
| 551 # TODO(luqui): test_data will need to use canonical unique names. | 557 # TODO(luqui): test_data will need to use canonical unique names. |
| 552 'test_data': test_data.get_module_test_data(mod.NAME) | 558 'test_data': test_data.get_module_test_data(mod.NAME) |
| 553 } | 559 } |
| 554 prop_defs = mod.PROPERTIES | 560 prop_defs = mod.PROPERTIES |
| 555 mod_api = invoke_with_properties( | 561 mod_api = invoke_with_properties( |
| 556 mod.API, engine.properties, prop_defs, **kwargs) | 562 mod.API, engine.properties, prop_defs, **kwargs) |
| 557 mod_api.test_api = (getattr(mod, 'TEST_API', None) | 563 mod_api.test_api = (getattr(mod, 'TEST_API', None) |
| 558 or RecipeTestApi)(module=mod) | 564 or RecipeTestApi)(module=mod) |
| 559 for k, v in deps.iteritems(): | 565 for k, v in deps.iteritems(): |
| 560 setattr(mod_api.m, k, v) | 566 setattr(mod_api.m, k, v) |
| 561 setattr(mod_api.test_api.m, k, v.test_api) | 567 setattr(mod_api.test_api.m, k, v.test_api) |
| 562 | 568 |
| 563 # Replace class-level Requirements placeholders in the recipe API with | 569 # Replace class-level Requirements placeholders in the recipe API with |
| 564 # their instance-level real values. | 570 # their instance-level real values. |
| 565 map(lambda (k, v): setattr(mod_api, k, _resolve_requirement(v, engine)), | 571 map(lambda (k, v): setattr(mod_api, k, _resolve_requirement(v, engine)), |
| 566 ((k, v) for k, v in type(mod_api).__dict__.iteritems() | 572 ((k, v) for k, v in type(mod_api).__dict__.iteritems() |
| 567 if isinstance(v, _UnresolvedRequirement))) | 573 if isinstance(v, _UnresolvedRequirement))) |
| 568 | 574 |
| 569 mod_api.initialize() | 575 mod_api.initialize() |
| 570 return mod_api | 576 return mod_api |
| 571 | 577 |
| 572 mapper = DependencyMapper(instantiator) | 578 mapper = DependencyMapper(instantiator) |
| 573 api = RecipeScriptApi(module=None, engine=engine, | 579 # Provide a fake module to the ScriptApi so that recipes can use: |
| 580 # * .name | |
| 581 # * .resource | |
| 582 # * .package_repo_resource | |
| 583 # This is obviously a hack, however it homogenizes the api and removes the | |
| 584 # need for some ugly workarounds in user code. A better way to do this would | |
| 585 # be to migrate all recipes to be members of modules. | |
| 586 | |
| 587 print toplevel_package.recipe_dirs | |
| 588 print recipe_script_path | |
|
nodir
2016/11/18 22:42:25
left by mistake?
iannucci
2016/11/19 00:00:57
Oops! 0:)
| |
| 589 name = os.path.join( | |
| 590 toplevel_package.name, | |
| 591 os.path.relpath( | |
| 592 os.path.splitext(recipe_script_path)[0], | |
| 593 toplevel_package.recipe_dirs[0])) | |
| 594 fakeModule = collections.namedtuple( | |
| 595 "fakeModule", "PACKAGE_REPO_ROOT NAME RESOURCE_DIRECTORY")( | |
| 596 Path(PackageRepoBasePath(toplevel_package)), | |
| 597 name, | |
| 598 Path(RecipeScriptBasePath( | |
| 599 name, os.path.splitext(recipe_script_path)[0]+".resources"))) | |
| 600 api = RecipeScriptApi(module=fakeModule, engine=engine, | |
| 574 test_data=test_data.get_module_test_data(None)) | 601 test_data=test_data.get_module_test_data(None)) |
| 575 for k, v in toplevel_deps.iteritems(): | 602 for k, v in toplevel_deps.iteritems(): |
| 576 setattr(api, k, mapper.instantiate(v)) | 603 setattr(api, k, mapper.instantiate(v)) |
| 577 return api | 604 return api |
| 578 | 605 |
| 579 | 606 |
| 580 def create_test_api(toplevel_deps, universe): | 607 def create_test_api(toplevel_deps, universe): |
| 581 def instantiator(mod, deps): | 608 def instantiator(mod, deps): |
| 582 modapi = (getattr(mod, 'TEST_API', None) or RecipeTestApi)(module=mod) | 609 modapi = (getattr(mod, 'TEST_API', None) or RecipeTestApi)(module=mod) |
| 583 for k,v in deps.iteritems(): | 610 for k,v in deps.iteritems(): |
| 584 setattr(modapi.m, k, v) | 611 setattr(modapi.m, k, v) |
| 585 return modapi | 612 return modapi |
| 586 | 613 |
| 587 mapper = DependencyMapper(instantiator) | 614 mapper = DependencyMapper(instantiator) |
| 588 api = RecipeTestApi(module=None) | 615 api = RecipeTestApi(module=None) |
| 589 for k,v in toplevel_deps.iteritems(): | 616 for k,v in toplevel_deps.iteritems(): |
| 590 setattr(api, k, mapper.instantiate(v)) | 617 setattr(api, k, mapper.instantiate(v)) |
| 591 return api | 618 return api |
| 592 | 619 |
| 593 | 620 |
| 594 def _resolve_requirement(req, engine): | 621 def _resolve_requirement(req, engine): |
| 595 if req._typ == 'client': | 622 if req._typ == 'client': |
| 596 return engine._get_client(req._name) | 623 return engine._get_client(req._name) |
| 597 else: | 624 else: |
| 598 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) | 625 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) |
| OLD | NEW |