Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(80)

Side by Side Diff: recipe_engine/loader.py

Issue 2512253002: Add name, package_repo_resource and resource support to recipe scripts. (Closed)
Patch Set: Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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,))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698