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

Side by Side Diff: recipe_engine/recipe_api.py

Issue 2332833003: Add better documentation, trigger namedtuple. (Closed)
Patch Set: More cleanups. Created 4 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
« no previous file with comments | « no previous file | recipe_engine/run.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 from __future__ import absolute_import 5 from __future__ import absolute_import
6 import contextlib 6 import contextlib
7 import collections 7 import collections
8 import keyword 8 import keyword
9 import re 9 import re
10 import types 10 import types
11 11
12 from functools import wraps 12 from functools import wraps
13 13
14 from .recipe_test_api import DisabledTestData, ModuleTestData 14 from .recipe_test_api import DisabledTestData, ModuleTestData
15 from .config import Single 15 from .config import Single
16 16
17 from .util import ModuleInjectionSite 17 from .util import ModuleInjectionSite
18 18
19 from . import field_composer 19 from . import field_composer
20 20
21 21
22 _StepConfig = collections.namedtuple('StepConfig', 22 _TriggerSpec = collections.namedtuple('_TriggerSpec',
23 ('bucket', 'builder_name', 'properties', 'buildbot_changes', 'tags',
24 'critical'))
25
26 class TriggerSpec(_TriggerSpec):
27 """
28 TriggerSpec is the internal representation of a raw trigger step. You should
29 use the standard 'step' recipe module, which will construct trigger specs
30 via API.
31 """
32
33 @classmethod
34 def _create(cls, builder_name, bucket=None, properties=None,
35 buildbot_changes=None, tags=None, critical=None):
36 """Creates a new TriggerSpec instance from its step API dictionary
37 keys/values.
38
39 Args:
40 builder_name (str): The name of the builder to trigger.
41 bucket (str or None): The name of the trigger bucket.
42 properties (dict or None): Key/value properties dictionary.
43 buildbot_changes (list or None): Optional list of BuildBot change dicts.
44 tags (list or None): Optional list of tag strings.
45 critical (bool or None): If true and triggering fails asynchronously, fail
46 the entire build. If None, the step defaults to being True.
47 """
48 if not isinstance(buildbot_changes, (types.NoneType, list)):
49 raise ValueError('buildbot_changes must be a list')
50
51 return cls(
52 bucket=bucket,
53 builder_name=builder_name,
54 properties=properties,
55 buildbot_changes=buildbot_changes,
56 tags=tags,
57 critical=bool(critical) if critical is not None else (True),
58 )
59
60 def _render_to_dict(self):
61 d = dict((k, v) for k, v in self._asdict().iteritems() if v)
62 if d['critical']:
63 d.pop('critical')
64 return d
65
66
67 _StepConfig = collections.namedtuple('_StepConfig',
23 ('name', 'cmd', 'cwd', 'env', 'allow_subannotations', 'trigger_specs', 68 ('name', 'cmd', 'cwd', 'env', 'allow_subannotations', 'trigger_specs',
24 'timeout', 'infra_step', 'stdout', 'stderr', 'stdin', 'ok_ret', 69 'timeout', 'infra_step', 'stdout', 'stderr', 'stdin', 'ok_ret',
25 'step_test_data', 'nest_level')) 70 'step_test_data', 'nest_level'))
26 71
27 class StepConfig(_StepConfig): 72 class StepConfig(_StepConfig):
28 """ 73 """
29 StepConfig parameters: 74 StepConfig is the representation of a raw step as the recipe_engine sees it.
30 name: name of the step, will appear in buildbots waterfall 75 You should use the standard 'step' recipe module, which will construct and
31 cmd: command to run, list of one or more strings 76 pass this data to the engine for you, instead. The only reason why you would
32 cwd: absolute path to working directory for the command 77 need to worry about this object is if you're modifying the step module itself.
33 env: dict with overrides for environment variables 78
34 allow_subannotations: if True, lets the step emit its own annotations 79 The optional "env" parameter provides optional overrides for environment
35 trigger_specs: a list of trigger specifications, see also _trigger_builds. 80 variables. Each value is % formatted with the entire existing os.environ. A
36 timeout: if not None, a datetime.timedelta for the step timeout. 81 value of `None` will remove that envvar from the environ. e.g.
37 infra_step: if True, this is an infrastructure step. 82
38 stdout: Path to a file to put step stdout into. If used, stdout won't 83 {
39 appear in annotator's stdout (and |allow_subannotations| is 84 "envvar": "%(envvar)s;%(envvar2)s;extra",
40 ignored). 85 "delete_this": None,
41 stderr: Path to a file to put step stderr into. If used, stderr won't 86 "static_value": "something",
42 appear in annotator's stderr. 87 }
43 stdin: Path to a file to read step stdin from.
44 ok_ret: Allowed return code list.
45 step_test_data: Possible associated step test data.
46 nest_level: the step's nesting level.
47 """ 88 """
48 89
90 _RENDER_WHITELIST=frozenset((
91 'cmd',
92 ))
49 93
50 def _make_step_config(**step): 94 _RENDER_BLACKLIST=frozenset((
51 """Galvanize a step dictionary into a formal StepConfig.""" 95 'nest_level',
52 step_config = StepConfig( 96 'ok_ret',
53 name=step.pop('name'), 97 'infra_step',
54 cmd=step.pop('cmd', None), 98 'step_test_data',
55 cwd=step.pop('cwd', None), 99 ))
56 env=step.pop('env', None), 100
57 allow_subannotations=step.pop('allow_subannotations', False), 101 @classmethod
58 trigger_specs=step.pop('trigger_specs', ()), 102 def create(cls, name, cmd=None, cwd=None, env=None,
59 timeout=step.pop('timeout', None), 103 allow_subannotations=None, trigger_specs=None, timeout=None,
60 infra_step=step.pop('infra_step', False), 104 infra_step=None, stdout=None, stderr=None, stdin=None,
61 stdout=step.pop('stdout', None), 105 ok_ret=None, step_test_data=None, step_nest_level=None):
62 stderr=step.pop('stderr', None), 106 """
63 stdin=step.pop('stdin', None), 107 Initializes a new StepConfig step API dictionary.
64 ok_ret=step.pop('ok_ret', {0}), 108
65 step_test_data=step.pop('step_test_data', None), 109 Args:
66 nest_level=step.pop('step_nest_level', 0), 110 name (str): name of the step, will appear in buildbots waterfall
67 ) 111 cmd: command to run. Acceptable types: str, Path, Placeholder, or None.
68 if step: 112 cwd (str or None): absolute path to working directory for the command
69 unknown_keys = sorted(step.iterkeys()) 113 env (dict): overrides for environment variables, described above.
70 raise KeyError('Unknown step dictionary keys: %s' % ( 114 allow_subannotations (bool): if True, lets the step emit its own
71 ', '.join(unknown_keys))) 115 annotations. NOTE: Enabling this can cause some buggy behavior. Please
72 return step_config 116 strongly consider using step_result.presentation instead. If you have
117 questions, please contact infra-dev@chromium.org.
118 trigger_specs: a list of trigger specifications, see also _trigger_builds.
119 timeout: if not None, a datetime.timedelta for the step timeout.
120 infra_step: if True, this is an infrastructure step. Failures will raise
121 InfraFailure instead of StepFailure.
122 stdout: Placeholder to put step stdout into. If used, stdout won't appear
123 in annotator's stdout (and |allow_subannotations| is ignored).
124 stderr: Placeholder to put step stderr into. If used, stderr won't appear
125 in annotator's stderr.
126 stdin: Placeholder to read step stdin from.
127 ok_ret (iter): set of return codes allowed. If the step process returns
128 something not on this list, it will raise a StepFailure (or
129 InfraFailure if infra_step is True). If omitted, {0} will be used.
130 step_test_data (func -> recipe_test_api.StepTestData): A factory which
131 returns a StepTestData object that will be used as the default test
132 data for this step. The recipe author can override/augment this object
133 in the GenTests function.
134 step_nest_level (int): the step's nesting level.
135 """
136 return cls(
137 name=name,
138 cmd=cmd,
139 cwd=cwd,
140 env=env,
141 allow_subannotations=bool(allow_subannotations),
142 trigger_specs=[TriggerSpec._create(**trig)
143 for trig in (trigger_specs or ())],
144 timeout=timeout,
145 infra_step=bool(infra_step),
146 stdout=stdout,
147 stderr=stderr,
148 stdin=stdin,
149 ok_ret=frozenset(ok_ret or (0,)),
150 step_test_data=step_test_data,
151 nest_level=int(step_nest_level or 0),
152 )
153
154 def render_to_dict(self):
155 self = self._replace(
156 trigger_specs=[trig._render_to_dict()
157 for trig in (self.trigger_specs or ())],
158 )
159 return dict((k, v) for k, v in self._asdict().iteritems()
160 if (v or k in self._RENDER_WHITELIST)
161 and k not in self._RENDER_BLACKLIST)
73 162
74 163
75 class StepFailure(Exception): 164 class StepFailure(Exception):
76 """ 165 """
77 This is the base class for all step failures. 166 This is the base class for all step failures.
78 167
79 Raising a StepFailure counts as 'running a step' for the purpose of 168 Raising a StepFailure counts as 'running a step' for the purpose of
80 infer_composite_step's logic. 169 infer_composite_step's logic.
81 """ 170 """
82 def __init__(self, name_or_reason, result=None): 171 def __init__(self, name_or_reason, result=None):
(...skipping 416 matching lines...) Expand 10 before | Expand all | Expand 10 after
499 return itm(base), params 588 return itm(base), params
500 except KeyError: 589 except KeyError:
501 if optional: 590 if optional:
502 return None, generic_params 591 return None, generic_params
503 else: # pragma: no cover 592 else: # pragma: no cover
504 raise # TODO(iannucci): raise a better exception. 593 raise # TODO(iannucci): raise a better exception.
505 594
506 def set_config(self, config_name=None, optional=False, **CONFIG_VARS): 595 def set_config(self, config_name=None, optional=False, **CONFIG_VARS):
507 """Sets the modules and its dependencies to the named configuration.""" 596 """Sets the modules and its dependencies to the named configuration."""
508 assert self._module 597 assert self._module
509 config, params = self.make_config_params(config_name, optional, 598 config, _ = self.make_config_params(config_name, optional, **CONFIG_VARS)
510 **CONFIG_VARS)
511 if config: 599 if config:
512 self.c = config 600 self.c = config
513 601
514 def apply_config(self, config_name, config_object=None, optional=False): 602 def apply_config(self, config_name, config_object=None, optional=False):
515 """Apply a named configuration to the provided config object or self.""" 603 """Apply a named configuration to the provided config object or self."""
516 assert config_name in self._module.CONFIG_CTX.CONFIG_ITEMS, ( 604 assert config_name in self._module.CONFIG_CTX.CONFIG_ITEMS, (
517 config_name, self._module.CONFIG_CTX.CONFIG_ITEMS) 605 config_name, self._module.CONFIG_CTX.CONFIG_ITEMS)
518 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name]( 606 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name](
519 config_object or self.c, optional=optional) 607 config_object or self.c, optional=optional)
520 608
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
598 686
599 if name in ('self',): 687 if name in ('self',):
600 return False 688 return False
601 689
602 if keyword.iskeyword(name): 690 if keyword.iskeyword(name):
603 return False 691 return False
604 692
605 regex = r'^[a-zA-Z][a-zA-Z0-9_]*$' if is_param_name else r'^[a-zA-Z][.\w]*$' 693 regex = r'^[a-zA-Z][a-zA-Z0-9_]*$' if is_param_name else r'^[a-zA-Z][.\w]*$'
606 return bool(re.match(regex, name)) 694 return bool(re.match(regex, name))
607 695
608 def __init__(self, default, help, kind, name, property_type, module, 696 def __init__(self, default, helptext, kind, name, property_type, module,
609 param_name=None): 697 param_name=None):
610 """ 698 """
611 Constructor for BoundProperty. 699 Constructor for BoundProperty.
612 700
613 Args: 701 Args:
614 default: The default value for this Property. Note: A default 702 default: The default value for this Property. Note: A default
615 value of None is allowed. To have no default value, omit 703 value of None is allowed. To have no default value, omit
616 this argument. 704 this argument.
617 help: The help text for this Property. 705 helptext: The help text for this Property.
618 kind: The type of this Property. You can either pass in a raw python 706 kind: The type of this Property. You can either pass in a raw python
619 type, or a Config Type, using the recipe engine config system. 707 type, or a Config Type, using the recipe engine config system.
620 name: The name of this Property. 708 name: The name of this Property.
621 param_name: The name of the python function parameter this property 709 param_name: The name of the python function parameter this property
622 should be stored in. Can be used to allow for dotted property 710 should be stored in. Can be used to allow for dotted property
623 names, e.g. 711 names, e.g.
624 PROPERTIES = { 712 PROPERTIES = {
625 'foo.bar.bam': Property(param_name="bizbaz") 713 'foo.bar.bam': Property(param_name="bizbaz")
626 } 714 }
627 module: The module this Property is a part of. 715 module: The module this Property is a part of.
628 """ 716 """
629 if not BoundProperty.legal_name(name): 717 if not BoundProperty.legal_name(name):
630 raise ValueError("Illegal name '{}'.".format(param_name)) 718 raise ValueError("Illegal name '{}'.".format(param_name))
631 719
632 param_name = param_name or name 720 param_name = param_name or name
633 if not BoundProperty.legal_name(param_name, is_param_name=True): 721 if not BoundProperty.legal_name(param_name, is_param_name=True):
634 raise ValueError("Illegal param_name '{}'.".format(param_name)) 722 raise ValueError("Illegal param_name '{}'.".format(param_name))
635 723
636 self.__default = default 724 self.__default = default
637 self.__help = help 725 self.__help = helptext
638 self.__kind = kind 726 self.__kind = kind
639 self.__name = name 727 self.__name = name
640 self.__property_type = property_type 728 self.__property_type = property_type
641 self.__param_name = param_name 729 self.__param_name = param_name
642 self.__module = module 730 self.__module = module
643 731
644 @property 732 @property
645 def name(self): 733 def name(self):
646 return self.__name 734 return self.__name
647 735
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
684 return value 772 return value
685 773
686 if self.default is not PROPERTY_SENTINEL: 774 if self.default is not PROPERTY_SENTINEL:
687 return self.default 775 return self.default
688 776
689 raise ValueError( 777 raise ValueError(
690 "No default specified and no value provided for '{}' from {} '{}'".format( 778 "No default specified and no value provided for '{}' from {} '{}'".format(
691 self.name, self.__property_type, self.module)) 779 self.name, self.__property_type, self.module))
692 780
693 class Property(object): 781 class Property(object):
694 def __init__(self, default=PROPERTY_SENTINEL, help="", kind=None, 782 def __init__(self, default=PROPERTY_SENTINEL, helptext="", kind=None,
695 param_name=None): 783 param_name=None):
696 """ 784 """
697 Constructor for Property. 785 Constructor for Property.
698 786
699 Args: 787 Args:
700 default: The default value for this Property. Note: A default 788 default: The default value for this Property. Note: A default
701 value of None is allowed. To have no default value, omit 789 value of None is allowed. To have no default value, omit
702 this argument. 790 this argument.
703 help: The help text for this Property. 791 helptext: The help text for this Property.
704 kind: The type of this Property. You can either pass in a raw python 792 kind: The type of this Property. You can either pass in a raw python
705 type, or a Config Type, using the recipe engine config system. 793 type, or a Config Type, using the recipe engine config system.
706 """ 794 """
707 self._default = default 795 self._default = default
708 self.help = help 796 self.helptext = helptext
martiniss 2016/09/13 17:09:16 This change is breaking all sorts of recipes downs
709 self.param_name = param_name 797 self.param_name = param_name
710 798
711 if isinstance(kind, type): 799 if isinstance(kind, type):
712 if kind in (str, unicode): 800 if kind in (str, unicode):
713 kind = basestring 801 kind = basestring
714 kind = Single(kind) 802 kind = Single(kind)
715 self.kind = kind 803 self.kind = kind
716 804
717 def bind(self, name, property_type, module): 805 def bind(self, name, property_type, module):
718 """ 806 """
719 Gets the BoundProperty version of this Property. Requires a name. 807 Gets the BoundProperty version of this Property. Requires a name.
720 """ 808 """
721 return BoundProperty( 809 return BoundProperty(
722 self._default, self.help, self.kind, name, property_type, module, 810 self._default, self.helptext, self.kind, name, property_type, module,
723 self.param_name) 811 self.param_name)
724 812
725 class UndefinedPropertyException(TypeError): 813 class UndefinedPropertyException(TypeError):
726 pass 814 pass
727 815
OLDNEW
« no previous file with comments | « no previous file | recipe_engine/run.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698