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

Unified Diff: third_party/recipe_engine/recipe_api.py

Issue 1347263002: Revert of Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 5 years, 3 months 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « third_party/recipe_engine/main.py ('k') | third_party/recipe_engine/recipe_test_api.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
new file mode 100644
index 0000000000000000000000000000000000000000..72661a9eda4b737a25d80f5c4b2ae27bedd0039c
--- /dev/null
+++ b/third_party/recipe_engine/recipe_api.py
@@ -0,0 +1,525 @@
+# 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
« no previous file with comments | « third_party/recipe_engine/main.py ('k') | third_party/recipe_engine/recipe_test_api.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698