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

Side by Side Diff: recipe_modules/context/api.py

Issue 2933473002: [context] Split "env" values and prefixes. (Closed)
Patch Set: comments Created 3 years, 6 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
OLDNEW
1 # Copyright 2017 The LUCI Authors. All rights reserved. 1 # Copyright 2017 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 """The context module provides APIs for manipulating a few pieces of 'ambient' 5 """The context module provides APIs for manipulating a few pieces of 'ambient'
6 data that affect how steps are run: 6 data that affect how steps are run:
7 cwd - The current working directory. 7 cwd - The current working directory.
8 env - The environment variables. 8 env - The environment variables.
9 infra_step - Whether or not failures should be treated as infrastructure 9 infra_step - Whether or not failures should be treated as infrastructure
10 failures vs. normal failures. 10 failures vs. normal failures.
(...skipping 24 matching lines...) Expand all
35 from recipe_engine.config_types import Path 35 from recipe_engine.config_types import Path
36 from recipe_engine.recipe_api import RecipeApi 36 from recipe_engine.recipe_api import RecipeApi
37 37
38 38
39 def check_type(name, var, expect): 39 def check_type(name, var, expect):
40 if not isinstance(var, expect): # pragma: no cover 40 if not isinstance(var, expect): # pragma: no cover
41 raise TypeError('%s is not %s: %r (%s)' % ( 41 raise TypeError('%s is not %s: %r (%s)' % (
42 name, expect.__name__, var, type(var).__name__)) 42 name, expect.__name__, var, type(var).__name__))
43 43
44 44
45 _EnvPathComponent = collections.namedtuple('_EnvPathComponent', (
46 'paths',))
47
48 # prefixes is a list of strings
49 # value is either a string or None
50 _EnvValue = collections.namedtuple('_EnvValue', (
51 'prefixes', 'value'))
52
53
54 class ContextApi(RecipeApi): 45 class ContextApi(RecipeApi):
55 46
56 # TODO(iannucci): move implementation of these data directly into this class. 47 # TODO(iannucci): move implementation of these data directly into this class.
57 def __init__(self, **kwargs): 48 def __init__(self, **kwargs):
58 super(RecipeApi, self).__init__(**kwargs) 49 super(RecipeApi, self).__init__(**kwargs)
59 50
60 self._cwd = [None] 51 self._cwd = [None]
52 self._env_prefixes = [{}]
61 self._env = [{}] 53 self._env = [{}]
62 self._infra_step = [False] 54 self._infra_step = [False]
63 self._name_prefix = [''] 55 self._name_prefix = ['']
64 # this could be a number, but it makes the logic easier to use a stack. 56 # this could be a number, but it makes the logic easier to use a stack.
65 self._nest_level = [0] 57 self._nest_level = [0]
66 58
67 @contextmanager 59 @contextmanager
68 def __call__(self, cwd=None, env=None, increment_nest_level=None, 60 def __call__(self, cwd=None, env_prefixes=None, env=None,
69 infra_steps=None, name_prefix=None): 61 increment_nest_level=None, infra_steps=None, name_prefix=None):
70 """Allows adjustment of multiple context values in a single call. 62 """Allows adjustment of multiple context values in a single call.
71 63
72 Contextual data: 64 Contextual data:
73 * cwd (Path) - the current working directory to use for all steps. 65 * cwd (Path) - the current working directory to use for all steps.
74 To 'reset' to the original cwd at the time recipes started, pass 66 To 'reset' to the original cwd at the time recipes started, pass
75 `api.path['start_dir']`. 67 `api.path['start_dir']`.
68 * env_prefixes (dict) - Environmental variable prefix augmentations. See
69 below for more info.
70 * env (dict) - Environmental variable overrides. See below for more info.
71 * increment_nest_level (True) - increment the nest level by 1 in this
72 context. Typically you won't directly interact with this, but should
73 use api.step.nest instead.
76 * infra_steps (bool) - if steps in this context should be considered 74 * infra_steps (bool) - if steps in this context should be considered
77 infrastructure steps. On failure, these will raise InfraFailure 75 infrastructure steps. On failure, these will raise InfraFailure
78 exceptions instead of StepFailure exceptions. 76 exceptions instead of StepFailure exceptions.
79 * increment_nest_level (True) - increment the nest level by 1 in this
80 context. Typically you won't directly interact with this, but should
81 use api.step.nest instead.
82 * name_prefix (str) - A string to prepend to the names of all steps in 77 * name_prefix (str) - A string to prepend to the names of all steps in
83 this context. These compose with '.' characters if multiple name prefix 78 this context. These compose with '.' characters if multiple name prefix
84 contexts occur. See below for more info. 79 contexts occur. See below for more info.
85 * env (dict) - Environmental variable overrides. See below for more info.
86 80
87 Name prefixes: 81 Name prefixes:
88 82
89 Multiple invocations concatenate values with '.'. 83 Multiple invocations concatenate values with '.'.
90 84
91 Example: 85 Example:
92 with api.context(name_prefix='hello'): 86 with api.context(name_prefix='hello'):
93 # has name 'hello.something' 87 # has name 'hello.something'
94 api.step('something', ['echo', 'something']) 88 api.step('something', ['echo', 'something'])
95 89
96 with api.context(name_prefix='world'): 90 with api.context(name_prefix='world'):
97 # has name 'hello.world.other' 91 # has name 'hello.world.other'
98 api.step('other', ['echo', 'other']) 92 api.step('other', ['echo', 'other'])
99 93
100 Environmental Variable Overrides: 94 Environmental Variable Overrides:
101 95
102 Env is a mapping of environment variable name to the value you want that 96 Env is a mapping of environment variable name to the value you want that
103 environment variable to have. The value is one of: 97 environment variable to have. The value is one of:
104 * None, indicating that the environment variable should be removed from 98 * None, indicating that the environment variable should be removed from
105 the environment when the step runs. 99 the environment when the step runs.
106 * A string value. Note that string values will be %-formatted with the 100 * A string value. Note that string values will be %-formatted with the
107 current value of the environment at the time the step runs. This means 101 current value of the environment at the time the step runs. This means
108 that you can have a value like: 102 that you can have a value like:
109 "/path/to/my/stuff:%(PATH)s" 103 "/path/to/my/stuff:%(PATH)s"
iannucci 2017/06/13 20:28:12 nuke this shite
dnj 2017/06/13 21:46:26 Done.
110 Which, at the time the step executes, will inject the current value of 104 Which, at the time the step executes, will inject the current value of
111 $PATH. 105 $PATH.
112 * A sentinel value such as Prefix to attach a specific component to a 106 * A sentinel value such as Prefix to attach a specific component to a
113 pathsep-delimited list variable. 107 pathsep-delimited list variable.
114 108
115 TODO(iannucci,dnj): Disallow "env" values to be mixes of string or 109 TODO(iannucci,dnj): Disallow "env" values to be mixes of string or
iannucci 2017/06/13 20:28:12 I think the prefix section got disappeared in a re
dnj 2017/06/13 21:46:26 Done.
116 Prefix/Suffix. 110 Prefix/Suffix.
117 111
118 TODO(iannucci): combine nest_level and name_prefix 112 TODO(iannucci): combine nest_level and name_prefix
119 113
120 Look at the examples in "examples/" for examples of context module usage. 114 Look at the examples in "examples/" for examples of context module usage.
121 """ 115 """
122 to_pop = [] 116 to_pop = []
117 def _push(st, val):
118 st.append(val)
119 to_pop.append(st)
123 120
124 if cwd is not None: 121 if cwd is not None:
125 check_type('cwd', cwd, Path) 122 check_type('cwd', cwd, Path)
126 self._cwd.append(cwd) 123 _push(self._cwd, cwd)
127 to_pop.append(self._cwd)
128 124
129 if infra_steps is not None: 125 if infra_steps is not None:
130 check_type('infra_steps', infra_steps, bool) 126 check_type('infra_steps', infra_steps, bool)
131 self._infra_step.append(infra_steps) 127 _push(self._infra_step, infra_steps)
132 to_pop.append(self._infra_step)
133 128
134 if increment_nest_level is not None: 129 if increment_nest_level is not None:
135 check_type('increment_nest_level', increment_nest_level, bool) 130 check_type('increment_nest_level', increment_nest_level, bool)
136 if not increment_nest_level: 131 if not increment_nest_level:
137 raise ValueError('increment_nest_level=False makes no sense') 132 raise ValueError('increment_nest_level=False makes no sense')
138 self._nest_level.append(self.nest_level+1) 133 _push(self._nest_level, self.nest_level+1)
139 to_pop.append(self._nest_level)
140 134
141 if name_prefix is not None: 135 if name_prefix is not None:
142 check_type('name_prefix', name_prefix, str) 136 check_type('name_prefix', name_prefix, str)
143 cur = self.name_prefix 137 cur = self.name_prefix
144 if cur: 138 if cur:
145 self._name_prefix.append('%s.%s' % (cur, name_prefix)) 139 name_prefix = '%s.%s' % (cur, name_prefix)
146 else: 140 _push(self._name_prefix, name_prefix)
147 self._name_prefix.append(name_prefix)
148 to_pop.append(self._name_prefix)
149 141
150 if env is not None and env != {}: 142 if env_prefixes is not None and len(env_prefixes) > 0:
143 check_type('env_prefixes', env_prefixes, dict)
144 new = dict(self._env_prefixes[-1])
145 for k, v in env_prefixes.iteritems():
146 k = str(k)
147 if not v:
148 continue
iannucci 2017/06/13 20:28:12 micro optimizaesezeze! (move if not v above str)
dnj 2017/06/13 21:46:26 Done.
149 new[k] = tuple(v) + new.get(k, ())
150 _push(self._env_prefixes, new)
151
152 if env is not None and len(env) > 0:
151 check_type('env', env, dict) 153 check_type('env', env, dict)
152 # we hit _env directly to avoid an extra copy. 154 # we hit _env directly to avoid an extra copy.
153 new = dict(self._env[-1]) 155 new = dict(self._env[-1])
154 for k, v in env.iteritems(): 156 for k, v in env.iteritems():
155 k = str(k) 157 k = str(k)
156 if v is None: 158 if v is not None:
157 ev = _EnvValue(prefixes=(), value=None) 159 v = str(v)
158 else: 160 try:
159 ev = new.get(k, _EnvValue(prefixes=(), value='')) 161 # This odd little piece of code does the following:
160 if isinstance(v, _EnvPathComponent): 162 # * add a bogus dictionary format %(foo)s to v. This forces %
161 ev = ev._replace(prefixes=v.paths+ev.prefixes) 163 # into 'dictionary lookup' mode
162 else: 164 # * format the result with a defaultdict. This allows all
163 v = str(v) 165 # `%(key)s` format lookups to succeed, but any sequential `%s`
164 try: 166 # lookups to fail.
165 # This odd little piece of code does the following: 167 # If the string contains any accidental sequential lookups, this
166 # * add a bogus dictionary format %(foo)s to v. This forces % 168 # will raise an exception. If not, then this is a pluasible format
167 # into 'dictionary lookup' mode 169 # string.
168 # * format the result with a defaultdict. This allows all 170 ('%(foo)s'+v) % collections.defaultdict(str)
169 # `%(key)s` format lookups to succeed, but any sequential `%s` 171 except Exception:
170 # lookups to fail. 172 raise ValueError(('Invalid %%-formatting parameter in envvar, '
171 # If the string contains any accidental sequential lookups, this 173 'only %%(ENVVAR)s allowed: %r') % (v,))
172 # will raise an exception. If not, then this is a pluasible format 174 new[k] = v
173 # string. 175 _push(self._env, new)
174 ('%(foo)s'+v) % collections.defaultdict(str)
175 except Exception:
176 raise ValueError(('Invalid %%-formatting parameter in envvar, '
177 'only %%(ENVVAR)s allowed: %r') % (v,))
178 ev = ev._replace(value=v)
179 new[k] = ev
180 self._env.append(new)
181 to_pop.append(self._env)
182 176
183 try: 177 try:
184 yield 178 yield
185 finally: 179 finally:
186 for p in to_pop: 180 for p in to_pop:
187 p.pop() 181 p.pop()
188 182
189 @property 183 @property
190 def cwd(self): 184 def cwd(self):
191 """Returns the current working directory that steps will run in. 185 """Returns the current working directory that steps will run in.
(...skipping 10 matching lines...) Expand all
202 196
203 By default this is empty; There's no facility to observe the program's 197 By default this is empty; There's no facility to observe the program's
204 startup environment. If you want to pass data to the recipe, it should be 198 startup environment. If you want to pass data to the recipe, it should be
205 done with properties. 199 done with properties.
206 200
207 Returns (dict) - The env-key -> value mapping of current environment 201 Returns (dict) - The env-key -> value mapping of current environment
208 modifications. 202 modifications.
209 """ 203 """
210 # TODO(iannucci): store env in an immutable way to avoid excessive copies. 204 # TODO(iannucci): store env in an immutable way to avoid excessive copies.
211 # TODO(iannucci): handle case-insensitive keys on windows 205 # TODO(iannucci): handle case-insensitive keys on windows
212 def parts(ev): 206 return dict(self._env[-1])
213 for p in ev.prefixes:
214 yield str(p)
215 if ev.value:
216 yield ev.value
217 207
218 ret = {} 208 @property
219 for k, ev in self._env[-1].iteritems(): 209 def env_prefixes(self):
220 if ev.value is None: 210 """Returns Path prefix modifications to the environment.
221 ret[k] = None
222 else:
223 ret[k] = self.m.path.pathsep.join(parts(ev))
224 211
225 return ret 212 This will return a mapping of environment key to Path tuple for Path
213 prefixes registered with the environment.
214
215 Returns (dict) - The env-key -> value(Path) mapping of current environment
216 prefix modifications.
217 """
218 # TODO(iannucci): store env in an immutable way to avoid excessive copies.
219 # TODO(iannucci): handle case-insensitive keys on windows
220 return dict(self._env_prefixes[-1])
226 221
227 @property 222 @property
228 def infra_step(self): 223 def infra_step(self):
229 """Returns the current value of the infra_step setting. 224 """Returns the current value of the infra_step setting.
230 225
231 Returns (bool) - True iff steps are currently considered infra steps. 226 Returns (bool) - True iff steps are currently considered infra steps.
232 """ 227 """
233 return self._infra_step[-1] 228 return self._infra_step[-1]
234 229
235 @property 230 @property
236 def name_prefix(self): 231 def name_prefix(self):
237 """Gets the current step name prefix. 232 """Gets the current step name prefix.
238 233
239 Returns (str) - The string prefix that every step will have prepended to it. 234 Returns (str) - The string prefix that every step will have prepended to it.
240 """ 235 """
241 return self._name_prefix[-1] 236 return self._name_prefix[-1]
242 237
243 @property 238 @property
244 def nest_level(self): 239 def nest_level(self):
245 """Returns the current 'nesting' level. 240 """Returns the current 'nesting' level.
246 241
247 Note: This api is low-level, and you should always prefer to use 242 Note: This api is low-level, and you should always prefer to use
248 `api.step.nest`. This api is included for completeness and documentation 243 `api.step.nest`. This api is included for completeness and documentation
249 purposes. 244 purposes.
250 245
251 Returns (int) - The current nesting level. 246 Returns (int) - The current nesting level.
252 """ 247 """
253 return self._nest_level[-1] 248 return self._nest_level[-1]
254
255 def Prefix(self, *paths):
256 """Returns: an assignable "env" value that prefixes the specified paths to
257 the beginning of an environment variable.
258
259 Each path in paths is added, in order, as a prefix to the environment
260 variable, delimited by the OS path separator. This can be used for
261 easy manipulation of path environment variables such as PATH and PYTHONPATH.
262
263 Args:
264 paths (...Path): The list of paths to prefix.
265 """
266 for i, path in enumerate(paths):
267 check_type('path element %d' % (i,), path, Path)
268 return _EnvPathComponent(paths=paths)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698