| Index: third_party/recipe_engine/recipe_api.py
|
| diff --git a/third_party/recipe_engine/recipe_api.py b/third_party/recipe_engine/recipe_api.py
|
| deleted file mode 100644
|
| index 72661a9eda4b737a25d80f5c4b2ae27bedd0039c..0000000000000000000000000000000000000000
|
| --- a/third_party/recipe_engine/recipe_api.py
|
| +++ /dev/null
|
| @@ -1,525 +0,0 @@
|
| -# Copyright 2013-2015 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -import contextlib
|
| -import keyword
|
| -import types
|
| -
|
| -from functools import wraps
|
| -
|
| -from .recipe_test_api import DisabledTestData, ModuleTestData
|
| -from .config import Single
|
| -
|
| -from .util import ModuleInjectionSite
|
| -
|
| -from . import field_composer
|
| -
|
| -
|
| -class StepFailure(Exception):
|
| - """
|
| - This is the base class for all step failures.
|
| -
|
| - Raising a StepFailure counts as 'running a step' for the purpose of
|
| - infer_composite_step's logic.
|
| - """
|
| - def __init__(self, name_or_reason, result=None):
|
| - _STEP_CONTEXT['ran_step'][0] = True
|
| - if result:
|
| - self.name = name_or_reason
|
| - self.result = result
|
| - self.reason = self.reason_message()
|
| - else:
|
| - self.name = None
|
| - self.result = None
|
| - self.reason = name_or_reason
|
| -
|
| - super(StepFailure, self).__init__(self.reason)
|
| -
|
| - def reason_message(self):
|
| - return "Step({!r}) failed with return_code {}".format(
|
| - self.name, self.result.retcode)
|
| -
|
| - def __str__(self): # pragma: no cover
|
| - return "Step Failure in %s" % self.name
|
| -
|
| - @property
|
| - def retcode(self):
|
| - """
|
| - Returns the retcode of the step which failed. If this was a manual
|
| - failure, returns None
|
| - """
|
| - if not self.result:
|
| - return None
|
| - return self.result.retcode
|
| -
|
| -
|
| -class StepWarning(StepFailure):
|
| - """
|
| - A subclass of StepFailure, which still fails the build, but which is
|
| - a warning. Need to figure out how exactly this will be useful.
|
| - """
|
| - def reason_message(self): # pragma: no cover
|
| - return "Warning: Step({!r}) returned {}".format(
|
| - self.name, self.result.retcode)
|
| -
|
| - def __str__(self): # pragma: no cover
|
| - return "Step Warning in %s" % self.name
|
| -
|
| -
|
| -class InfraFailure(StepFailure):
|
| - """
|
| - A subclass of StepFailure, which fails the build due to problems with the
|
| - infrastructure.
|
| - """
|
| - def reason_message(self):
|
| - return "Infra Failure: Step({!r}) returned {}".format(
|
| - self.name, self.result.retcode)
|
| -
|
| - def __str__(self):
|
| - return "Infra Failure in %s" % self.name
|
| -
|
| -
|
| -class AggregatedStepFailure(StepFailure):
|
| - def __init__(self, result):
|
| - super(AggregatedStepFailure, self).__init__(
|
| - "Aggregate step failure.", result=result)
|
| -
|
| - def reason_message(self):
|
| - msg = "{!r} out of {!r} aggregated steps failed. Failures: ".format(
|
| - len(self.result.failures), len(self.result.all_results))
|
| - msg += ', '.join((f.reason or f.name) for f in self.result.failures)
|
| - return msg
|
| -
|
| - def __str__(self): # pragma: no cover
|
| - return "Aggregate Step Failure"
|
| -
|
| -
|
| -_FUNCTION_REGISTRY = {
|
| - 'aggregated_result': {'combine': lambda a, b: b},
|
| - 'env': {'combine': lambda a, b: dict(a, **b)},
|
| - 'name': {'combine': lambda a, b: '%s.%s' % (a, b)},
|
| - 'nest_level': {'combine': lambda a, b: a + b},
|
| - 'ran_step': {'combine': lambda a, b: b},
|
| -}
|
| -
|
| -
|
| -class AggregatedResult(object):
|
| - """Holds the result of an aggregated run of steps.
|
| -
|
| - Currently this is only used internally by defer_results, but it may be exposed
|
| - to the consumer of defer_results at some point in the future. For now it's
|
| - expected to be easier for defer_results consumers to do their own result
|
| - aggregation, as they may need to pick and chose (or label) which results they
|
| - really care about.
|
| - """
|
| - def __init__(self):
|
| - self.successes = []
|
| - self.failures = []
|
| -
|
| - # Needs to be here to be able to treat this as a step result
|
| - self.retcode = None
|
| -
|
| - @property
|
| - def all_results(self):
|
| - """
|
| - Return a list of two item tuples (x, y), where
|
| - x is whether or not the step succeeded, and
|
| - y is the result of the run
|
| - """
|
| - res = [(True, result) for result in self.successes]
|
| - res.extend([(False, result) for result in self.failures])
|
| - return res
|
| -
|
| - def add_success(self, result):
|
| - self.successes.append(result)
|
| -
|
| - def add_failure(self, exception):
|
| - self.failures.append(exception)
|
| -
|
| -
|
| -class DeferredResult(object):
|
| - def __init__(self, result, failure):
|
| - self._result = result
|
| - self._failure = failure
|
| -
|
| - @property
|
| - def is_ok(self):
|
| - return self._failure is None
|
| -
|
| - def get_result(self):
|
| - if not self.is_ok:
|
| - raise self.get_error()
|
| - return self._result
|
| -
|
| - def get_error(self):
|
| - assert self._failure, "WHAT IS IT ARE YOU DOING???!?!?!? SHTAP NAO"
|
| - return self._failure
|
| -
|
| -
|
| -_STEP_CONTEXT = field_composer.FieldComposer(
|
| - {'ran_step': [False]}, _FUNCTION_REGISTRY)
|
| -
|
| -
|
| -def non_step(func):
|
| - """A decorator which prevents a method from automatically being wrapped as
|
| - a infer_composite_step by RecipeApiMeta.
|
| -
|
| - This is needed for utility methods which don't run any steps, but which are
|
| - invoked within the context of a defer_results().
|
| -
|
| - @see infer_composite_step, defer_results, RecipeApiMeta
|
| - """
|
| - assert not hasattr(func, "_skip_inference"), \
|
| - "Double-wrapped method %r?" % func
|
| - func._skip_inference = True # pylint: disable=protected-access
|
| - return func
|
| -
|
| -_skip_inference = non_step
|
| -
|
| -
|
| -@contextlib.contextmanager
|
| -def context(fields):
|
| - global _STEP_CONTEXT
|
| - old = _STEP_CONTEXT
|
| - try:
|
| - _STEP_CONTEXT = old.compose(fields)
|
| - yield
|
| - finally:
|
| - _STEP_CONTEXT = old
|
| -
|
| -
|
| -def infer_composite_step(func):
|
| - """A decorator which possibly makes this step act as a single step, for the
|
| - purposes of the defer_results function.
|
| -
|
| - Behaves as if this function were wrapped by composite_step, unless this
|
| - function:
|
| - * is already wrapped by non_step
|
| - * returns a result without calling api.step
|
| - * raises an exception which is not derived from StepFailure
|
| -
|
| - In any of these cases, this function will behave like a normal function.
|
| -
|
| - This decorator is automatically applied by RecipeApiMeta (or by inheriting
|
| - from RecipeApi). If you want to decalare a method's behavior explicitly, you
|
| - may decorate it with either composite_step or with non_step.
|
| - """
|
| - if getattr(func, "_skip_inference", False):
|
| - return func
|
| -
|
| - @_skip_inference # to prevent double-wraps
|
| - @wraps(func)
|
| - def _inner(*a, **kw):
|
| - # We're not in a defer_results context, so just run the function normally.
|
| - if _STEP_CONTEXT.get('aggregated_result') is None:
|
| - return func(*a, **kw)
|
| -
|
| - agg = _STEP_CONTEXT['aggregated_result']
|
| -
|
| - # Setting the aggregated_result to None allows the contents of func to be
|
| - # written in the same style (e.g. with exceptions) no matter how func is
|
| - # being called.
|
| - with context({'aggregated_result': None, 'ran_step': [False]}):
|
| - try:
|
| - ret = func(*a, **kw)
|
| - if not _STEP_CONTEXT.get('ran_step', [False])[0]:
|
| - return ret
|
| - agg.add_success(ret)
|
| - return DeferredResult(ret, None)
|
| - except StepFailure as ex:
|
| - agg.add_failure(ex)
|
| - return DeferredResult(None, ex)
|
| - return _inner
|
| -
|
| -
|
| -def composite_step(func):
|
| - """A decorator which makes this step act as a single step, for the purposes of
|
| - the defer_results function.
|
| -
|
| - This means that this function will not quit during the middle of its execution
|
| - because of a StepFailure, if there is an aggregator active.
|
| -
|
| - You may use this decorator explicitly if infer_composite_step is detecting
|
| - the behavior of your method incorrectly to force it to behave as a step. You
|
| - may also need to use this if your Api class inherits from RecipeApiPlain and
|
| - so doesn't have its methods automatically wrapped by infer_composite_step.
|
| - """
|
| - @_skip_inference # to avoid double-wraps
|
| - @wraps(func)
|
| - def _inner(*a, **kw):
|
| - # always counts as running a step
|
| - _STEP_CONTEXT['ran_step'][0] = True
|
| -
|
| - if _STEP_CONTEXT.get('aggregated_result') is None:
|
| - return func(*a, **kw)
|
| -
|
| - agg = _STEP_CONTEXT['aggregated_result']
|
| -
|
| - # Setting the aggregated_result to None allows the contents of func to be
|
| - # written in the same style (e.g. with exceptions) no matter how func is
|
| - # being called.
|
| - with context({'aggregated_result': None}):
|
| - try:
|
| - ret = func(*a, **kw)
|
| - agg.add_success(ret)
|
| - return DeferredResult(ret, None)
|
| - except StepFailure as ex:
|
| - agg.add_failure(ex)
|
| - return DeferredResult(None, ex)
|
| - return _inner
|
| -
|
| -
|
| -@contextlib.contextmanager
|
| -def defer_results():
|
| - """
|
| - Use this to defer step results in your code. All steps which would previously
|
| - return a result or throw an exception will instead return a DeferredResult.
|
| -
|
| - Any exceptions which were thrown during execution will be thrown when either:
|
| - a. You call get_result() on the step's result.
|
| - b. You exit the suite inside of the with statement
|
| -
|
| - Example:
|
| - with defer_results():
|
| - api.step('a', ..)
|
| - api.step('b', ..)
|
| - result = api.m.module.im_a_composite_step(...)
|
| - api.m.echo('the data is', result.get_result())
|
| -
|
| - If 'a' fails, 'b' and 'im a composite step' will still run.
|
| - If 'im a composite step' fails, then the get_result() call will raise
|
| - an exception.
|
| - If you don't try to use the result (don't call get_result()), an aggregate
|
| - failure will still be raised once you exit the suite inside
|
| - the with statement.
|
| - """
|
| - assert _STEP_CONTEXT.get('aggregated_result') is None, (
|
| - "may not call defer_results in an active defer_results context")
|
| - agg = AggregatedResult()
|
| - with context({'aggregated_result': agg}):
|
| - yield
|
| - if agg.failures:
|
| - raise AggregatedStepFailure(agg)
|
| -
|
| -
|
| -class RecipeApiMeta(type):
|
| - WHITELIST = ('__init__',)
|
| - def __new__(mcs, name, bases, attrs):
|
| - """Automatically wraps all methods of subclasses of RecipeApi with
|
| - @infer_composite_step. This allows defer_results to work as intended without
|
| - manually decorating every method.
|
| - """
|
| - wrap = lambda f: infer_composite_step(f) if f else f
|
| - for attr in attrs:
|
| - if attr in RecipeApiMeta.WHITELIST:
|
| - continue
|
| - val = attrs[attr]
|
| - if isinstance(val, types.FunctionType):
|
| - attrs[attr] = wrap(val)
|
| - elif isinstance(val, property):
|
| - attrs[attr] = property(
|
| - wrap(val.fget),
|
| - wrap(val.fset),
|
| - wrap(val.fdel),
|
| - val.__doc__)
|
| - return super(RecipeApiMeta, mcs).__new__(mcs, name, bases, attrs)
|
| -
|
| -
|
| -class RecipeApiPlain(ModuleInjectionSite):
|
| - """
|
| - Framework class for handling recipe_modules.
|
| -
|
| - Inherit from this in your recipe_modules/<name>/api.py . This class provides
|
| - wiring for your config context (in self.c and methods, and for dependency
|
| - injection (in self.m).
|
| -
|
| - Dependency injection takes place in load_recipe_modules() in recipe_loader.py.
|
| -
|
| - USE RecipeApi INSTEAD, UNLESS your RecipeApi subclass derives from something
|
| - which defines its own __metaclass__. Deriving from RecipeApi instead of
|
| - RecipeApiPlain allows your RecipeApi subclass to automatically work with
|
| - defer_results without needing to decorate every methods with
|
| - @infer_composite_step.
|
| - """
|
| -
|
| - def __init__(self, module=None, engine=None,
|
| - test_data=DisabledTestData(), **_kwargs):
|
| - """Note: Injected dependencies are NOT available in __init__()."""
|
| - super(RecipeApiPlain, self).__init__()
|
| -
|
| - # |engine| is an instance of annotated_run.RecipeEngine. Modules should not
|
| - # generally use it unless they're low-level framework level modules.
|
| - self._engine = engine
|
| - self._module = module
|
| -
|
| - assert isinstance(test_data, (ModuleTestData, DisabledTestData))
|
| - self._test_data = test_data
|
| -
|
| - # If we're the 'root' api, inject directly into 'self'.
|
| - # Otherwise inject into 'self.m'
|
| - self.m = self if module is None else ModuleInjectionSite(self)
|
| -
|
| - # If our module has a test api, it gets injected here.
|
| - self.test_api = None
|
| -
|
| - # Config goes here.
|
| - self.c = None
|
| -
|
| - def get_config_defaults(self): # pylint: disable=R0201
|
| - """
|
| - Allows your api to dynamically determine static default values for configs.
|
| - """
|
| - return {}
|
| -
|
| - def make_config(self, config_name=None, optional=False, **CONFIG_VARS):
|
| - """Returns a 'config blob' for the current API."""
|
| - return self.make_config_params(config_name, optional, **CONFIG_VARS)[0]
|
| -
|
| - def make_config_params(self, config_name, optional=False, **CONFIG_VARS):
|
| - """Returns a 'config blob' for the current API, and the computed params
|
| - for all dependent configurations.
|
| -
|
| - The params have the following order of precendence. Each subsequent param
|
| - is dict.update'd into the final parameters, so the order is from lowest to
|
| - higest precedence on a per-key basis:
|
| - * if config_name in CONFIG_CTX
|
| - * get_config_defaults()
|
| - * CONFIG_CTX[config_name].DEFAULT_CONFIG_VARS()
|
| - * CONFIG_VARS
|
| - * else
|
| - * get_config_defaults()
|
| - * CONFIG_VARS
|
| - """
|
| - generic_params = self.get_config_defaults() # generic defaults
|
| - generic_params.update(CONFIG_VARS) # per-invocation values
|
| -
|
| - ctx = self._module.CONFIG_CTX
|
| - if optional and not ctx:
|
| - return None, generic_params
|
| -
|
| - assert ctx, '%s has no config context' % self
|
| - try:
|
| - params = self.get_config_defaults() # generic defaults
|
| - itm = ctx.CONFIG_ITEMS[config_name] if config_name else None
|
| - if itm:
|
| - params.update(itm.DEFAULT_CONFIG_VARS()) # per-item defaults
|
| - params.update(CONFIG_VARS) # per-invocation values
|
| -
|
| - base = ctx.CONFIG_SCHEMA(**params)
|
| - if config_name is None:
|
| - return base, params
|
| - else:
|
| - return itm(base), params
|
| - except KeyError:
|
| - if optional:
|
| - return None, generic_params
|
| - else: # pragma: no cover
|
| - raise # TODO(iannucci): raise a better exception.
|
| -
|
| - def set_config(self, config_name=None, optional=False, **CONFIG_VARS):
|
| - """Sets the modules and its dependencies to the named configuration."""
|
| - assert self._module
|
| - config, params = self.make_config_params(config_name, optional,
|
| - **CONFIG_VARS)
|
| - if config:
|
| - self.c = config
|
| -
|
| - def apply_config(self, config_name, config_object=None, optional=False):
|
| - """Apply a named configuration to the provided config object or self."""
|
| - self._module.CONFIG_CTX.CONFIG_ITEMS[config_name](
|
| - config_object or self.c, optional=optional)
|
| -
|
| - def resource(self, *path):
|
| - """Returns path to a file under <recipe module>/resources/ directory.
|
| -
|
| - Args:
|
| - path: path relative to module's resources/ directory.
|
| - """
|
| - # TODO(vadimsh): Verify that file exists. Including a case like:
|
| - # module.resource('dir').join('subdir', 'file.py')
|
| - return self._module.MODULE_DIRECTORY.join('resources', *path)
|
| -
|
| - @property
|
| - def name(self):
|
| - return self._module.NAME
|
| -
|
| -
|
| -class RecipeApi(RecipeApiPlain):
|
| - __metaclass__ = RecipeApiMeta
|
| -
|
| -
|
| -class Property(object):
|
| - sentinel = object()
|
| -
|
| - @staticmethod
|
| - def legal_name(name):
|
| - if name.startswith('_'):
|
| - return False
|
| -
|
| - if name in ('self',):
|
| - return False
|
| -
|
| - if keyword.iskeyword(name):
|
| - return False
|
| -
|
| - return True
|
| -
|
| - @property
|
| - def name(self):
|
| - return self._name
|
| -
|
| - @name.setter
|
| - def name(self, name):
|
| - if not Property.legal_name(name):
|
| - raise ValueError("Illegal name '{}'".format(name))
|
| -
|
| - self._name = name
|
| -
|
| - def __init__(self, default=sentinel, help="", kind=None):
|
| - """
|
| - Constructor for Property.
|
| -
|
| - Args:
|
| - default: The default value for this Property. Note: A default
|
| - value of None is allowed. To have no default value, omit
|
| - this argument.
|
| - help: The help text for this Property.
|
| - type: The type of this Property. You can either pass in a raw python
|
| - type, or a Config Type, using the recipe engine config system.
|
| - """
|
| - self._default = default
|
| - self.help = help
|
| - self._name = None
|
| -
|
| - if isinstance(kind, type):
|
| - kind = Single(kind)
|
| - self.kind = kind
|
| -
|
| - def interpret(self, value):
|
| - """
|
| - Interprets the value for this Property.
|
| -
|
| - Args:
|
| - value: The value to interpret. May be None, which
|
| - means no value provided.
|
| -
|
| - Returns:
|
| - The value to use for this property. Raises an error if
|
| - this property has no valid interpretation.
|
| - """
|
| - if value is not Property.sentinel:
|
| - if self.kind is not None:
|
| - # The config system handles type checking for us here.
|
| - self.kind.set_val(value)
|
| - return value
|
| -
|
| - if self._default is not Property.sentinel:
|
| - return self._default
|
| -
|
| - raise ValueError(
|
| - "No default specified and no value provided for '{}'".format(
|
| - self.name))
|
| -
|
| -class UndefinedPropertyException(TypeError):
|
| - pass
|
|
|