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 |