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

Side by Side Diff: third_party/recipe_engine/recipe_api.py

Issue 1241323004: Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Roll to latest recipes-py 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 unified diff | Download patch | Annotate | Revision Log
« 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 »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2013-2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 import contextlib
6 import keyword
7 import types
8
9 from functools import wraps
10
11 from .recipe_test_api import DisabledTestData, ModuleTestData
12 from .config import Single
13
14 from .util import ModuleInjectionSite
15
16 from . import field_composer
17
18
19 class StepFailure(Exception):
20 """
21 This is the base class for all step failures.
22
23 Raising a StepFailure counts as 'running a step' for the purpose of
24 infer_composite_step's logic.
25 """
26 def __init__(self, name_or_reason, result=None):
27 _STEP_CONTEXT['ran_step'][0] = True
28 if result:
29 self.name = name_or_reason
30 self.result = result
31 self.reason = self.reason_message()
32 else:
33 self.name = None
34 self.result = None
35 self.reason = name_or_reason
36
37 super(StepFailure, self).__init__(self.reason)
38
39 def reason_message(self):
40 return "Step({!r}) failed with return_code {}".format(
41 self.name, self.result.retcode)
42
43 def __str__(self): # pragma: no cover
44 return "Step Failure in %s" % self.name
45
46 @property
47 def retcode(self):
48 """
49 Returns the retcode of the step which failed. If this was a manual
50 failure, returns None
51 """
52 if not self.result:
53 return None
54 return self.result.retcode
55
56
57 class StepWarning(StepFailure):
58 """
59 A subclass of StepFailure, which still fails the build, but which is
60 a warning. Need to figure out how exactly this will be useful.
61 """
62 def reason_message(self): # pragma: no cover
63 return "Warning: Step({!r}) returned {}".format(
64 self.name, self.result.retcode)
65
66 def __str__(self): # pragma: no cover
67 return "Step Warning in %s" % self.name
68
69
70 class InfraFailure(StepFailure):
71 """
72 A subclass of StepFailure, which fails the build due to problems with the
73 infrastructure.
74 """
75 def reason_message(self):
76 return "Infra Failure: Step({!r}) returned {}".format(
77 self.name, self.result.retcode)
78
79 def __str__(self):
80 return "Infra Failure in %s" % self.name
81
82
83 class AggregatedStepFailure(StepFailure):
84 def __init__(self, result):
85 super(AggregatedStepFailure, self).__init__(
86 "Aggregate step failure.", result=result)
87
88 def reason_message(self):
89 msg = "{!r} out of {!r} aggregated steps failed. Failures: ".format(
90 len(self.result.failures), len(self.result.all_results))
91 msg += ', '.join((f.reason or f.name) for f in self.result.failures)
92 return msg
93
94 def __str__(self): # pragma: no cover
95 return "Aggregate Step Failure"
96
97
98 _FUNCTION_REGISTRY = {
99 'aggregated_result': {'combine': lambda a, b: b},
100 'env': {'combine': lambda a, b: dict(a, **b)},
101 'name': {'combine': lambda a, b: '%s.%s' % (a, b)},
102 'nest_level': {'combine': lambda a, b: a + b},
103 'ran_step': {'combine': lambda a, b: b},
104 }
105
106
107 class AggregatedResult(object):
108 """Holds the result of an aggregated run of steps.
109
110 Currently this is only used internally by defer_results, but it may be exposed
111 to the consumer of defer_results at some point in the future. For now it's
112 expected to be easier for defer_results consumers to do their own result
113 aggregation, as they may need to pick and chose (or label) which results they
114 really care about.
115 """
116 def __init__(self):
117 self.successes = []
118 self.failures = []
119
120 # Needs to be here to be able to treat this as a step result
121 self.retcode = None
122
123 @property
124 def all_results(self):
125 """
126 Return a list of two item tuples (x, y), where
127 x is whether or not the step succeeded, and
128 y is the result of the run
129 """
130 res = [(True, result) for result in self.successes]
131 res.extend([(False, result) for result in self.failures])
132 return res
133
134 def add_success(self, result):
135 self.successes.append(result)
136
137 def add_failure(self, exception):
138 self.failures.append(exception)
139
140
141 class DeferredResult(object):
142 def __init__(self, result, failure):
143 self._result = result
144 self._failure = failure
145
146 @property
147 def is_ok(self):
148 return self._failure is None
149
150 def get_result(self):
151 if not self.is_ok:
152 raise self.get_error()
153 return self._result
154
155 def get_error(self):
156 assert self._failure, "WHAT IS IT ARE YOU DOING???!?!?!? SHTAP NAO"
157 return self._failure
158
159
160 _STEP_CONTEXT = field_composer.FieldComposer(
161 {'ran_step': [False]}, _FUNCTION_REGISTRY)
162
163
164 def non_step(func):
165 """A decorator which prevents a method from automatically being wrapped as
166 a infer_composite_step by RecipeApiMeta.
167
168 This is needed for utility methods which don't run any steps, but which are
169 invoked within the context of a defer_results().
170
171 @see infer_composite_step, defer_results, RecipeApiMeta
172 """
173 assert not hasattr(func, "_skip_inference"), \
174 "Double-wrapped method %r?" % func
175 func._skip_inference = True # pylint: disable=protected-access
176 return func
177
178 _skip_inference = non_step
179
180
181 @contextlib.contextmanager
182 def context(fields):
183 global _STEP_CONTEXT
184 old = _STEP_CONTEXT
185 try:
186 _STEP_CONTEXT = old.compose(fields)
187 yield
188 finally:
189 _STEP_CONTEXT = old
190
191
192 def infer_composite_step(func):
193 """A decorator which possibly makes this step act as a single step, for the
194 purposes of the defer_results function.
195
196 Behaves as if this function were wrapped by composite_step, unless this
197 function:
198 * is already wrapped by non_step
199 * returns a result without calling api.step
200 * raises an exception which is not derived from StepFailure
201
202 In any of these cases, this function will behave like a normal function.
203
204 This decorator is automatically applied by RecipeApiMeta (or by inheriting
205 from RecipeApi). If you want to decalare a method's behavior explicitly, you
206 may decorate it with either composite_step or with non_step.
207 """
208 if getattr(func, "_skip_inference", False):
209 return func
210
211 @_skip_inference # to prevent double-wraps
212 @wraps(func)
213 def _inner(*a, **kw):
214 # We're not in a defer_results context, so just run the function normally.
215 if _STEP_CONTEXT.get('aggregated_result') is None:
216 return func(*a, **kw)
217
218 agg = _STEP_CONTEXT['aggregated_result']
219
220 # Setting the aggregated_result to None allows the contents of func to be
221 # written in the same style (e.g. with exceptions) no matter how func is
222 # being called.
223 with context({'aggregated_result': None, 'ran_step': [False]}):
224 try:
225 ret = func(*a, **kw)
226 if not _STEP_CONTEXT.get('ran_step', [False])[0]:
227 return ret
228 agg.add_success(ret)
229 return DeferredResult(ret, None)
230 except StepFailure as ex:
231 agg.add_failure(ex)
232 return DeferredResult(None, ex)
233 return _inner
234
235
236 def composite_step(func):
237 """A decorator which makes this step act as a single step, for the purposes of
238 the defer_results function.
239
240 This means that this function will not quit during the middle of its execution
241 because of a StepFailure, if there is an aggregator active.
242
243 You may use this decorator explicitly if infer_composite_step is detecting
244 the behavior of your method incorrectly to force it to behave as a step. You
245 may also need to use this if your Api class inherits from RecipeApiPlain and
246 so doesn't have its methods automatically wrapped by infer_composite_step.
247 """
248 @_skip_inference # to avoid double-wraps
249 @wraps(func)
250 def _inner(*a, **kw):
251 # always counts as running a step
252 _STEP_CONTEXT['ran_step'][0] = True
253
254 if _STEP_CONTEXT.get('aggregated_result') is None:
255 return func(*a, **kw)
256
257 agg = _STEP_CONTEXT['aggregated_result']
258
259 # Setting the aggregated_result to None allows the contents of func to be
260 # written in the same style (e.g. with exceptions) no matter how func is
261 # being called.
262 with context({'aggregated_result': None}):
263 try:
264 ret = func(*a, **kw)
265 agg.add_success(ret)
266 return DeferredResult(ret, None)
267 except StepFailure as ex:
268 agg.add_failure(ex)
269 return DeferredResult(None, ex)
270 return _inner
271
272
273 @contextlib.contextmanager
274 def defer_results():
275 """
276 Use this to defer step results in your code. All steps which would previously
277 return a result or throw an exception will instead return a DeferredResult.
278
279 Any exceptions which were thrown during execution will be thrown when either:
280 a. You call get_result() on the step's result.
281 b. You exit the suite inside of the with statement
282
283 Example:
284 with defer_results():
285 api.step('a', ..)
286 api.step('b', ..)
287 result = api.m.module.im_a_composite_step(...)
288 api.m.echo('the data is', result.get_result())
289
290 If 'a' fails, 'b' and 'im a composite step' will still run.
291 If 'im a composite step' fails, then the get_result() call will raise
292 an exception.
293 If you don't try to use the result (don't call get_result()), an aggregate
294 failure will still be raised once you exit the suite inside
295 the with statement.
296 """
297 assert _STEP_CONTEXT.get('aggregated_result') is None, (
298 "may not call defer_results in an active defer_results context")
299 agg = AggregatedResult()
300 with context({'aggregated_result': agg}):
301 yield
302 if agg.failures:
303 raise AggregatedStepFailure(agg)
304
305
306 class RecipeApiMeta(type):
307 WHITELIST = ('__init__',)
308 def __new__(mcs, name, bases, attrs):
309 """Automatically wraps all methods of subclasses of RecipeApi with
310 @infer_composite_step. This allows defer_results to work as intended without
311 manually decorating every method.
312 """
313 wrap = lambda f: infer_composite_step(f) if f else f
314 for attr in attrs:
315 if attr in RecipeApiMeta.WHITELIST:
316 continue
317 val = attrs[attr]
318 if isinstance(val, types.FunctionType):
319 attrs[attr] = wrap(val)
320 elif isinstance(val, property):
321 attrs[attr] = property(
322 wrap(val.fget),
323 wrap(val.fset),
324 wrap(val.fdel),
325 val.__doc__)
326 return super(RecipeApiMeta, mcs).__new__(mcs, name, bases, attrs)
327
328
329 class RecipeApiPlain(ModuleInjectionSite):
330 """
331 Framework class for handling recipe_modules.
332
333 Inherit from this in your recipe_modules/<name>/api.py . This class provides
334 wiring for your config context (in self.c and methods, and for dependency
335 injection (in self.m).
336
337 Dependency injection takes place in load_recipe_modules() in recipe_loader.py.
338
339 USE RecipeApi INSTEAD, UNLESS your RecipeApi subclass derives from something
340 which defines its own __metaclass__. Deriving from RecipeApi instead of
341 RecipeApiPlain allows your RecipeApi subclass to automatically work with
342 defer_results without needing to decorate every methods with
343 @infer_composite_step.
344 """
345
346 def __init__(self, module=None, engine=None,
347 test_data=DisabledTestData(), **_kwargs):
348 """Note: Injected dependencies are NOT available in __init__()."""
349 super(RecipeApiPlain, self).__init__()
350
351 # |engine| is an instance of annotated_run.RecipeEngine. Modules should not
352 # generally use it unless they're low-level framework level modules.
353 self._engine = engine
354 self._module = module
355
356 assert isinstance(test_data, (ModuleTestData, DisabledTestData))
357 self._test_data = test_data
358
359 # If we're the 'root' api, inject directly into 'self'.
360 # Otherwise inject into 'self.m'
361 self.m = self if module is None else ModuleInjectionSite(self)
362
363 # If our module has a test api, it gets injected here.
364 self.test_api = None
365
366 # Config goes here.
367 self.c = None
368
369 def get_config_defaults(self): # pylint: disable=R0201
370 """
371 Allows your api to dynamically determine static default values for configs.
372 """
373 return {}
374
375 def make_config(self, config_name=None, optional=False, **CONFIG_VARS):
376 """Returns a 'config blob' for the current API."""
377 return self.make_config_params(config_name, optional, **CONFIG_VARS)[0]
378
379 def make_config_params(self, config_name, optional=False, **CONFIG_VARS):
380 """Returns a 'config blob' for the current API, and the computed params
381 for all dependent configurations.
382
383 The params have the following order of precendence. Each subsequent param
384 is dict.update'd into the final parameters, so the order is from lowest to
385 higest precedence on a per-key basis:
386 * if config_name in CONFIG_CTX
387 * get_config_defaults()
388 * CONFIG_CTX[config_name].DEFAULT_CONFIG_VARS()
389 * CONFIG_VARS
390 * else
391 * get_config_defaults()
392 * CONFIG_VARS
393 """
394 generic_params = self.get_config_defaults() # generic defaults
395 generic_params.update(CONFIG_VARS) # per-invocation values
396
397 ctx = self._module.CONFIG_CTX
398 if optional and not ctx:
399 return None, generic_params
400
401 assert ctx, '%s has no config context' % self
402 try:
403 params = self.get_config_defaults() # generic defaults
404 itm = ctx.CONFIG_ITEMS[config_name] if config_name else None
405 if itm:
406 params.update(itm.DEFAULT_CONFIG_VARS()) # per-item defaults
407 params.update(CONFIG_VARS) # per-invocation values
408
409 base = ctx.CONFIG_SCHEMA(**params)
410 if config_name is None:
411 return base, params
412 else:
413 return itm(base), params
414 except KeyError:
415 if optional:
416 return None, generic_params
417 else: # pragma: no cover
418 raise # TODO(iannucci): raise a better exception.
419
420 def set_config(self, config_name=None, optional=False, **CONFIG_VARS):
421 """Sets the modules and its dependencies to the named configuration."""
422 assert self._module
423 config, params = self.make_config_params(config_name, optional,
424 **CONFIG_VARS)
425 if config:
426 self.c = config
427
428 def apply_config(self, config_name, config_object=None, optional=False):
429 """Apply a named configuration to the provided config object or self."""
430 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name](
431 config_object or self.c, optional=optional)
432
433 def resource(self, *path):
434 """Returns path to a file under <recipe module>/resources/ directory.
435
436 Args:
437 path: path relative to module's resources/ directory.
438 """
439 # TODO(vadimsh): Verify that file exists. Including a case like:
440 # module.resource('dir').join('subdir', 'file.py')
441 return self._module.MODULE_DIRECTORY.join('resources', *path)
442
443 @property
444 def name(self):
445 return self._module.NAME
446
447
448 class RecipeApi(RecipeApiPlain):
449 __metaclass__ = RecipeApiMeta
450
451
452 class Property(object):
453 sentinel = object()
454
455 @staticmethod
456 def legal_name(name):
457 if name.startswith('_'):
458 return False
459
460 if name in ('self',):
461 return False
462
463 if keyword.iskeyword(name):
464 return False
465
466 return True
467
468 @property
469 def name(self):
470 return self._name
471
472 @name.setter
473 def name(self, name):
474 if not Property.legal_name(name):
475 raise ValueError("Illegal name '{}'".format(name))
476
477 self._name = name
478
479 def __init__(self, default=sentinel, help="", kind=None):
480 """
481 Constructor for Property.
482
483 Args:
484 default: The default value for this Property. Note: A default
485 value of None is allowed. To have no default value, omit
486 this argument.
487 help: The help text for this Property.
488 type: The type of this Property. You can either pass in a raw python
489 type, or a Config Type, using the recipe engine config system.
490 """
491 self._default = default
492 self.help = help
493 self._name = None
494
495 if isinstance(kind, type):
496 kind = Single(kind)
497 self.kind = kind
498
499 def interpret(self, value):
500 """
501 Interprets the value for this Property.
502
503 Args:
504 value: The value to interpret. May be None, which
505 means no value provided.
506
507 Returns:
508 The value to use for this property. Raises an error if
509 this property has no valid interpretation.
510 """
511 if value is not Property.sentinel:
512 if self.kind is not None:
513 # The config system handles type checking for us here.
514 self.kind.set_val(value)
515 return value
516
517 if self._default is not Property.sentinel:
518 return self._default
519
520 raise ValueError(
521 "No default specified and no value provided for '{}'".format(
522 self.name))
523
524 class UndefinedPropertyException(TypeError):
525 pass
OLDNEW
« 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