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

Side by Side Diff: recipe_engine/loader.py

Issue 2512253002: Add name, package_repo_resource and resource support to recipe scripts. (Closed)
Patch Set: Fix bug, add additional test 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 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
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
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
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
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,))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698