| 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 functools |
| 6 import imp | 7 import imp |
| 7 import inspect | 8 import inspect |
| 8 import os | 9 import os |
| 9 import sys | 10 import sys |
| 10 | 11 |
| 11 from . import env | 12 from . import env |
| 12 | 13 |
| 13 from .config import ConfigContext, ConfigGroupSchema | 14 from .config import ConfigContext, ConfigGroupSchema |
| 14 from .config_types import Path, ModuleBasePath, PackageRepoBasePath | 15 from .config_types import Path, ModuleBasePath, PackageRepoBasePath |
| 15 from .config_types import RecipeScriptBasePath | 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 RecipeApiPlain, RecipeScriptApi | 18 from .recipe_api import RecipeApiPlain, RecipeScriptApi |
| 18 from .recipe_api import _UnresolvedRequirement | 19 from .recipe_api import _UnresolvedRequirement |
| 19 from .recipe_api import BoundProperty | 20 from .recipe_api import BoundProperty |
| 20 from .recipe_api import UndefinedPropertyException, PROPERTY_SENTINEL | 21 from .recipe_api import UndefinedPropertyException, PROPERTY_SENTINEL |
| 22 from .recipe_api import StepFailure |
| 21 from .recipe_test_api import RecipeTestApi, DisabledTestData | 23 from .recipe_test_api import RecipeTestApi, DisabledTestData |
| 24 from .types import StepDataAttributeError |
| 25 import subprocess42 |
| 22 | 26 |
| 23 | 27 |
| 24 class LoaderError(Exception): | 28 class LoaderError(Exception): |
| 25 """Raised when something goes wrong loading recipes or modules.""" | 29 """Raised when something goes wrong loading recipes or modules.""" |
| 26 | 30 |
| 27 | 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 |
| 38 class RecipeException(Exception): |
| 39 """Raised when an unhandeled exception is raised in a recipe.""" |
| 40 def __init__(self, inner_exception, traceback): |
| 41 self.inner_exception = inner_exception |
| 42 self.traceback = traceback |
| 43 super(RecipeException, self).__init__(inner_exception.message) |
| 44 |
| 45 |
| 34 class RecipeScript(object): | 46 class RecipeScript(object): |
| 35 """Holds dict of an evaluated recipe script.""" | 47 """Holds dict of an evaluated recipe script.""" |
| 36 | 48 |
| 37 def __init__(self, recipe_globals, path): | 49 def __init__(self, recipe_globals, path): |
| 38 self.path = path | 50 self.path = path |
| 39 self._recipe_globals = recipe_globals | 51 self._recipe_globals = recipe_globals |
| 40 | 52 |
| 41 self.run_steps, self.gen_tests = [ | 53 self.run_steps, self.gen_tests = [ |
| 42 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] | 54 recipe_globals.get(k) for k in ('RunSteps', 'GenTests')] |
| 43 | 55 |
| (...skipping 25 matching lines...) Expand all Loading... |
| 69 return self._recipe_globals['LOADED_DEPS'] | 81 return self._recipe_globals['LOADED_DEPS'] |
| 70 | 82 |
| 71 @property | 83 @property |
| 72 def RETURN_SCHEMA(self): | 84 def RETURN_SCHEMA(self): |
| 73 return self._recipe_globals.get('RETURN_SCHEMA') | 85 return self._recipe_globals.get('RETURN_SCHEMA') |
| 74 | 86 |
| 75 def run(self, api, properties): | 87 def run(self, api, properties): |
| 76 """ | 88 """ |
| 77 Run this recipe, with the given api and property arguments. | 89 Run this recipe, with the given api and property arguments. |
| 78 Check the return value, if we have a RETURN_SCHEMA. | 90 Check the return value, if we have a RETURN_SCHEMA. |
| 91 |
| 92 Raises: |
| 93 RecipeException if an unhandled exception happened in the recipe. |
| 94 TypeError if recipe did not return an expected result. |
| 79 """ | 95 """ |
| 96 |
| 97 orig_run_steps = self.run_steps |
| 98 |
| 99 def run_steps(*args, **kwargs): |
| 100 try: |
| 101 return orig_run_steps(*args, **kwargs) |
| 102 except ( |
| 103 StepFailure, |
| 104 StepDataAttributeError, |
| 105 subprocess42.TimeoutExpired): |
| 106 raise |
| 107 except Exception as ex: |
| 108 raise RecipeException(ex, sys.exc_info()[2]) |
| 109 |
| 80 recipe_result = invoke_with_properties( | 110 recipe_result = invoke_with_properties( |
| 81 self.run_steps, properties, self.PROPERTIES, api=api) | 111 run_steps, properties, self.PROPERTIES, |
| 112 inspectable_obj=orig_run_steps, |
| 113 api=api) |
| 82 | 114 |
| 83 if self.RETURN_SCHEMA: | 115 if self.RETURN_SCHEMA: |
| 84 if not recipe_result: | 116 if not recipe_result: |
| 85 raise ValueError("Recipe %s did not return a value." % self.name) | 117 raise ValueError("Recipe %s did not return a value." % self.name) |
| 86 return recipe_result.as_jsonish(True) | 118 return recipe_result.as_jsonish(True) |
| 87 else: | 119 else: |
| 88 return None | 120 return None |
| 89 | 121 |
| 90 @classmethod | 122 @classmethod |
| 91 def from_script_path(cls, script_path, universe_view): | 123 def from_script_path(cls, script_path, universe_view): |
| (...skipping 447 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 539 "Missing property value for '{}'.".format(prop_name)) | 571 "Missing property value for '{}'.".format(prop_name)) |
| 540 | 572 |
| 541 prop = prop_defs[prop_name] | 573 prop = prop_defs[prop_name] |
| 542 props.append(prop.interpret(all_props.get( | 574 props.append(prop.interpret(all_props.get( |
| 543 prop_name, PROPERTY_SENTINEL))) | 575 prop_name, PROPERTY_SENTINEL))) |
| 544 | 576 |
| 545 return callable_obj(*props, **additional_args) | 577 return callable_obj(*props, **additional_args) |
| 546 | 578 |
| 547 | 579 |
| 548 def invoke_with_properties(callable_obj, all_props, prop_defs, | 580 def invoke_with_properties(callable_obj, all_props, prop_defs, |
| 549 **additional_args): | 581 inspectable_obj=None, **additional_args): |
| 550 """ | 582 """ |
| 551 Invokes callable with filtered, type-checked properties. | 583 Invokes callable with filtered, type-checked properties. |
| 552 | 584 |
| 553 Args: | 585 Args: |
| 554 callable_obj: The function to call, or class to instantiate. | 586 callable_obj: The function to call, or class to instantiate. |
| 555 This supports passing in either RunSteps, or a recipe module, | 587 This supports passing in either RunSteps, or a recipe module, |
| 556 which is a class. | 588 which is a class. |
| 557 all_props: A dictionary containing all the properties (strings) currently | 589 all_props: A dictionary containing all the properties (strings) currently |
| 558 defined in the system. | 590 defined in the system. |
| 559 prop_defs: A dictionary of property name to property definitions | 591 prop_defs: A dictionary of property name to property definitions |
| 560 (BoundProperty) for this callable. | 592 (BoundProperty) for this callable. |
| 593 inspectable_obj: a function to inspect instead of callable_obj. |
| 594 Useful when callback_obj is wrapped. |
| 561 additional_args: kwargs to pass through to the callable. | 595 additional_args: kwargs to pass through to the callable. |
| 562 Note that the names of the arguments can correspond to | 596 Note that the names of the arguments can correspond to |
| 563 positional arguments as well. | 597 positional arguments as well. |
| 564 | 598 |
| 565 Returns: | 599 Returns: |
| 566 The result of calling callable with the filtered properties | 600 The result of calling callable with the filtered properties |
| 567 and additional arguments. | 601 and additional arguments. |
| 568 """ | 602 """ |
| 569 # Check that we got passed BoundProperties, and not Properties | 603 # Check that we got passed BoundProperties, and not Properties |
| 570 | 604 |
| 605 inspectable_obj = inspectable_obj or callable_obj |
| 606 |
| 571 | 607 |
| 572 # To detect when they didn't specify a property that they have as a | 608 # To detect when they didn't specify a property that they have as a |
| 573 # function argument, list the arguments, through inspection, | 609 # function argument, list the arguments, through inspection, |
| 574 # and then comparing this list to the provided properties. We use a list | 610 # and then comparing this list to the provided properties. We use a list |
| 575 # instead of a dict because getargspec returns a list which we would have to | 611 # instead of a dict because getargspec returns a list which we would have to |
| 576 # convert to a dictionary, and the benefit of the dictionary is pretty small. | 612 # convert to a dictionary, and the benefit of the dictionary is pretty small. |
| 577 if inspect.isclass(callable_obj): | 613 if inspect.isclass(inspectable_obj): |
| 578 arg_names = inspect.getargspec(callable_obj.__init__).args | 614 arg_names = inspect.getargspec(inspectable_obj.__init__).args |
| 579 | 615 |
| 580 arg_names.pop(0) | 616 arg_names.pop(0) |
| 581 else: | 617 else: |
| 582 arg_names = inspect.getargspec(callable_obj).args | 618 arg_names = inspect.getargspec(inspectable_obj).args |
| 583 return _invoke_with_properties(callable_obj, all_props, prop_defs, arg_names, | 619 return _invoke_with_properties(callable_obj, all_props, prop_defs, arg_names, |
| 584 **additional_args) | 620 **additional_args) |
| 585 | 621 |
| 586 | 622 |
| 587 def create_recipe_api(toplevel_package, toplevel_deps, recipe_script_path, | 623 def create_recipe_api(toplevel_package, toplevel_deps, recipe_script_path, |
| 588 engine, test_data=DisabledTestData()): | 624 engine, test_data=DisabledTestData()): |
| 589 def instantiator(mod, deps): | 625 def instantiator(mod, deps): |
| 590 kwargs = { | 626 kwargs = { |
| 591 'module': mod, | 627 'module': mod, |
| 592 # TODO(luqui): test_data will need to use canonical unique names. | 628 # TODO(luqui): test_data will need to use canonical unique names. |
| (...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 662 for k,v in toplevel_deps.iteritems(): | 698 for k,v in toplevel_deps.iteritems(): |
| 663 setattr(api, k, mapper.instantiate(v)) | 699 setattr(api, k, mapper.instantiate(v)) |
| 664 return api | 700 return api |
| 665 | 701 |
| 666 | 702 |
| 667 def _resolve_requirement(req, engine): | 703 def _resolve_requirement(req, engine): |
| 668 if req._typ == 'client': | 704 if req._typ == 'client': |
| 669 return engine._get_client(req._name) | 705 return engine._get_client(req._name) |
| 670 else: | 706 else: |
| 671 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) | 707 raise ValueError('Unknown requirement type [%s]' % (req._typ,)) |
| OLD | NEW |