| 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 imp | 6 import imp |
| 7 import inspect | 7 import inspect |
| 8 import os | 8 import os |
| 9 import sys | 9 import sys |
| 10 | 10 |
| 11 from . import env | 11 from . import env |
| 12 | 12 |
| 13 from .config import ConfigContext, ConfigGroupSchema | 13 from .config import ConfigContext, ConfigGroupSchema |
| 14 from .config_types import Path, ModuleBasePath, PackageRepoBasePath | 14 from .config_types import Path, ModuleBasePath, PackageRepoBasePath |
| 15 from .config_types import RecipeScriptBasePath | 15 from .config_types import RecipeScriptBasePath |
| 16 from .config_types import RECIPE_MODULE_PREFIX | 16 from .config_types import RECIPE_MODULE_PREFIX |
| 17 from .recipe_api import RecipeApiPlain, RecipeScriptApi | 17 from .recipe_api import RecipeApi, RecipeApiPlain, RecipeScriptApi |
| 18 from .recipe_api import _UnresolvedRequirement | 18 from .recipe_api import _UnresolvedRequirement |
| 19 from .recipe_api import BoundProperty | 19 from .recipe_api import BoundProperty |
| 20 from .recipe_api import UndefinedPropertyException, PROPERTY_SENTINEL | 20 from .recipe_api import UndefinedPropertyException, PROPERTY_SENTINEL |
| 21 from .recipe_test_api import RecipeTestApi, DisabledTestData | 21 from .recipe_test_api import RecipeTestApi, DisabledTestData |
| 22 | 22 |
| 23 | 23 |
| 24 class LoaderError(Exception): | 24 class LoaderError(Exception): |
| 25 """Raised when something goes wrong loading recipes or modules.""" | 25 """Raised when something goes wrong loading recipes or modules.""" |
| 26 | 26 |
| 27 | 27 |
| 28 class MalformedRecipeError(LoaderError): |
| 29 """Raised when a recipe doesn't conform to the expected interface.""" |
| 30 |
| 31 |
| 28 class NoSuchRecipe(LoaderError): | 32 class NoSuchRecipe(LoaderError): |
| 29 """Raised by load_recipe is recipe is not found.""" | 33 """Raised by load_recipe is recipe is not found.""" |
| 30 def __init__(self, recipe): | 34 def __init__(self, recipe): |
| 31 super(NoSuchRecipe, self).__init__('No such recipe: %s' % recipe) | 35 super(NoSuchRecipe, self).__init__('No such recipe: %s' % recipe) |
| 32 | 36 |
| 33 | 37 |
| 34 class RecipeScript(object): | 38 class RecipeScript(object): |
| 35 """Holds dict of an evaluated recipe script.""" | 39 """Holds dict of an evaluated recipe script.""" |
| 36 | 40 |
| 37 def __init__(self, name, recipe_globals, package_name, path): | 41 def __init__(self, name, recipe_globals, package_name, path): |
| 38 self._path = path | 42 self._path = path |
| 39 self._name = name | 43 self._name = name |
| 40 self._recipe_globals = recipe_globals | 44 self._recipe_globals = recipe_globals |
| 41 | 45 |
| 42 self.run_steps, self.gen_tests = [ | 46 self.run_steps, self.gen_tests = [ |
| 43 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] | 47 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] |
| 44 | 48 |
| 49 if not self.run_steps: |
| 50 raise MalformedRecipeError('Missing or misspelled RunSteps function.') |
| 51 |
| 52 if not self.gen_tests: |
| 53 raise MalformedRecipeError('Missing or misspelled GenTests function.') |
| 54 |
| 45 # Let each property object know about the property name. | 55 # Let each property object know about the property name. |
| 46 full_decl_name = '%s::%s' % (package_name, name) | 56 full_decl_name = '%s::%s' % (package_name, name) |
| 47 recipe_globals['PROPERTIES'] = { | 57 recipe_globals['PROPERTIES'] = { |
| 48 name: value.bind( | 58 name: value.bind( |
| 49 name, BoundProperty.RECIPE_PROPERTY, full_decl_name) | 59 name, BoundProperty.RECIPE_PROPERTY, full_decl_name) |
| 50 for name, value in recipe_globals.get('PROPERTIES', {}).items()} | 60 for name, value in recipe_globals.get('PROPERTIES', {}).items()} |
| 51 | 61 |
| 52 return_schema = recipe_globals.get('RETURN_SCHEMA') | 62 return_schema = recipe_globals.get('RETURN_SCHEMA') |
| 53 if return_schema and not isinstance(return_schema, ConfigGroupSchema): | 63 if return_schema and not isinstance(return_schema, ConfigGroupSchema): |
| 54 raise ValueError('Invalid RETURN_SCHEMA; must be an instance of ' | 64 raise MalformedRecipeError( |
| 55 'ConfigGroupSchema') | 65 'Invalid RETURN_SCHEMA; must be an instance of ConfigGroupSchema') |
| 66 |
| 56 | 67 |
| 57 @property | 68 @property |
| 58 def path(self): | 69 def path(self): |
| 59 return self._path | 70 return self._path |
| 60 | 71 |
| 61 @property | 72 @property |
| 62 def name(self): | 73 def name(self): |
| 63 return self._name | 74 return self._name |
| 64 | 75 |
| 65 @property | 76 @property |
| (...skipping 381 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 447 assert not submod.CONFIG_CTX, ( | 458 assert not submod.CONFIG_CTX, ( |
| 448 'More than one configuration context: %s, %s' % | 459 'More than one configuration context: %s, %s' % |
| 449 (submod.config, submod.CONFIG_CTX)) | 460 (submod.config, submod.CONFIG_CTX)) |
| 450 submod.CONFIG_CTX = v | 461 submod.CONFIG_CTX = v |
| 451 assert submod.CONFIG_CTX, 'Config file, but no config context?' | 462 assert submod.CONFIG_CTX, 'Config file, but no config context?' |
| 452 | 463 |
| 453 # Identify the RecipeApiPlain subclass as this module's API. | 464 # Identify the RecipeApiPlain subclass as this module's API. |
| 454 submod.API = getattr(submod, 'API', None) | 465 submod.API = getattr(submod, 'API', None) |
| 455 assert hasattr(submod, 'api'), '%s is missing api.py' % (name,) | 466 assert hasattr(submod, 'api'), '%s is missing api.py' % (name,) |
| 456 for v in submod.api.__dict__.itervalues(): | 467 for v in submod.api.__dict__.itervalues(): |
| 468 # If the recipe has literally imported the RecipeApi, we don't wan't |
| 469 # to consider that to be the real RecipeApi :) |
| 470 if v is RecipeApiPlain or v is RecipeApi: |
| 471 continue |
| 457 if inspect.isclass(v) and issubclass(v, RecipeApiPlain): | 472 if inspect.isclass(v) and issubclass(v, RecipeApiPlain): |
| 458 assert not submod.API, ( | 473 assert not submod.API, ( |
| 459 '%s has more than one RecipeApi subclass: %s, %s' % ( | 474 '%s has more than one RecipeApi subclass: %s, %s' % ( |
| 460 name, v, submod.API)) | 475 name, v, submod.API)) |
| 461 submod.API = v | 476 submod.API = v |
| 462 assert submod.API, 'Submodule has no api? %s' % (submod) | 477 assert submod.API, 'Submodule has no api? %s' % (submod) |
| 463 | 478 |
| 464 # Identify the (optional) RecipeTestApi subclass as this module's test API. | 479 # Identify the (optional) RecipeTestApi subclass as this module's test API. |
| 465 submod.TEST_API = getattr(submod, 'TEST_API', None) | 480 submod.TEST_API = getattr(submod, 'TEST_API', None) |
| 466 if hasattr(submod, 'test_api'): | 481 if hasattr(submod, 'test_api'): |
| 467 for v in submod.test_api.__dict__.itervalues(): | 482 for v in submod.test_api.__dict__.itervalues(): |
| 483 # If the recipe has literally imported the RecipeTestApi, we don't wan't |
| 484 # to consider that to be the real RecipeTestApi :) |
| 485 if v is RecipeTestApi: |
| 486 continue |
| 468 if inspect.isclass(v) and issubclass(v, RecipeTestApi): | 487 if inspect.isclass(v) and issubclass(v, RecipeTestApi): |
| 469 assert not submod.TEST_API, ( | 488 assert not submod.TEST_API, ( |
| 470 'More than one TestApi subclass: %s' % submod.api) | 489 'More than one TestApi subclass: %s' % submod.api) |
| 471 submod.TEST_API = v | 490 submod.TEST_API = v |
| 472 assert submod.API, ( | 491 assert submod.API, ( |
| 473 'Submodule has test_api.py but no TestApi subclass? %s' | 492 'Submodule has test_api.py but no TestApi subclass? %s' |
| 474 % (submod) | 493 % (submod) |
| 475 ) | 494 ) |
| 476 | 495 |
| 477 # Let each property object know about the property name. | 496 # Let each property object know about the property name. |
| (...skipping 200 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 678 for k,v in toplevel_deps.iteritems(): | 697 for k,v in toplevel_deps.iteritems(): |
| 679 setattr(api, k, mapper.instantiate(v)) | 698 setattr(api, k, mapper.instantiate(v)) |
| 680 return api | 699 return api |
| 681 | 700 |
| 682 | 701 |
| 683 def _resolve_requirement(req, engine): | 702 def _resolve_requirement(req, engine): |
| 684 if req._typ == 'client': | 703 if req._typ == 'client': |
| 685 return engine._get_client(req._name) | 704 return engine._get_client(req._name) |
| 686 else: | 705 else: |
| 687 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) | 706 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) |
| OLD | NEW |