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

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

Issue 2925453002: [context] Add path prefix/suffix manipulation. (Closed)
Patch Set: comments, use sentinels 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.
11 name_prefix - A prefix for all step names. 11 name_prefix - A prefix for all step names.
12 nest_level - An indicator for the UI of how deeply to nest steps. 12 nest_level - An indicator for the UI of how deeply to nest steps.
13 13
14 The values here are all scoped using Python's `with` statement; there's no 14 The values here are all scoped using Python's `with` statement; there's no
15 mechanism to make an open-ended adjustment to these values (i.e. there's no way 15 mechanism to make an open-ended adjustment to these values (i.e. there's no way
16 to change the cwd permanently for a recipe, except by surrounding the entire 16 to change the cwd permanently for a recipe, except by surrounding the entire
17 recipe with a with statement). This is done to avoid the surprises that 17 recipe with a with statement). This is done to avoid the surprises that
18 typically arise with things like os.environ or os.chdir in a normal python 18 typically arise with things like os.environ or os.chdir in a normal python
19 program. 19 program.
20 20
21 Example: 21 Example:
22 with api.context(cwd=api.path['start_dir'].join('subdir')): 22 with api.context(cwd=api.path['start_dir'].join('subdir')):
23 # this step is run inside of the subdir directory. 23 # this step is run inside of the subdir directory.
24 api.step("cat subdir/foo", ['cat', './foo']) 24 api.step("cat subdir/foo", ['cat', './foo'])
25 """ 25 """
26 26
27 27
28 import collections 28 import collections
29 import os
30 import types
29 31
30 from contextlib import contextmanager 32 from contextlib import contextmanager
31 33
32 from recipe_engine import recipe_api 34 from recipe_engine import recipe_api
33 from recipe_engine.config_types import Path 35 from recipe_engine.config_types import Path
34 from recipe_engine.recipe_api import RecipeApi 36 from recipe_engine.recipe_api import RecipeApi
35 37
36 38
37 def check_type(name, var, expect): 39 def check_type(name, var, expect):
38 if not isinstance(var, expect): # pragma: no cover 40 if not isinstance(var, expect): # pragma: no cover
39 raise TypeError('%s is not %s: %r (%s)' % ( 41 raise TypeError('%s is not %s: %r (%s)' % (
40 name, expect.__name__, var, type(var).__name__)) 42 name, expect.__name__, var, type(var).__name__))
41 43
42 44
45 _EnvPathComponent = collections.namedtuple('_EnvPathComponent', (
46 'paths', 'is_prefix'))
47
48 _EnvValue = collections.namedtuple('_EnvValue', (
49 'str', 'prefixes', 'suffixes'))
iannucci 2017/06/07 23:17:59 prefixes, str, suffixes?
dnj 2017/06/07 23:48:46 Done.
50
51
43 class ContextApi(RecipeApi): 52 class ContextApi(RecipeApi):
53
44 # TODO(iannucci): move implementation of these data directly into this class. 54 # TODO(iannucci): move implementation of these data directly into this class.
45 def __init__(self, **kwargs): 55 def __init__(self, **kwargs):
46 super(RecipeApi, self).__init__(**kwargs) 56 super(RecipeApi, self).__init__(**kwargs)
47 57
48 self._cwd = [None] 58 self._cwd = [None]
49 self._env = [{}] 59 self._env = [{}]
50 self._infra_step = [False] 60 self._infra_step = [False]
51 self._name_prefix = [''] 61 self._name_prefix = ['']
52 # this could be a number, but it makes the logic easier to use a stack. 62 # this could be a number, but it makes the logic easier to use a stack.
53 self._nest_level = [0] 63 self._nest_level = [0]
(...skipping 27 matching lines...) Expand all
81 # has name 'hello.something' 91 # has name 'hello.something'
82 api.step('something', ['echo', 'something']) 92 api.step('something', ['echo', 'something'])
83 93
84 with api.context(name_prefix='world'): 94 with api.context(name_prefix='world'):
85 # has name 'hello.world.other' 95 # has name 'hello.world.other'
86 api.step('other', ['echo', 'other']) 96 api.step('other', ['echo', 'other'])
87 97
88 Environmental Variable Overrides: 98 Environmental Variable Overrides:
89 99
90 Env is a mapping of environment variable name to the value you want that 100 Env is a mapping of environment variable name to the value you want that
91 environment variable to have. The value is a string, with a couple 101 environment variable to have. The value is either:
iannucci 2017/06/07 23:17:59 s/either/one of
dnj 2017/06/07 23:48:46 Done.
92 exceptions: 102 * None, indicating that the environment variable should be removed from
93 * If value is None, this environment variable will be removed from the 103 the environment when the step runs.
94 environment when the step runs. 104 * A string value. Note that string values will be %-formatted with the
95 * String values will be %-formatted with the current value of the 105 current value of the environment at the time the step runs. This means
96 environment at the time the step runs. This means that you can have 106 that you can have a value like:
97 a value like: 107 "/path/to/my/stuff:%(PATH)s"
98 "/path/to/my/stuff:%(PATH)s"
99 Which, at the time the step executes, will inject the current value of 108 Which, at the time the step executes, will inject the current value of
100 $PATH. 109 $PATH.
110 * A sentinel value such as Prefix or Suffix to attach a specific component
111 to a pathsep-delimited list variable.
101 112
102 TODO(iannucci): implement env_paths which allows for easier manipulation of 113 TODO(iannucci,dnj): Disallow "env" values to be mixes of string or
103 `pathsep` environment variables like $PATH, $PYTHONPATH, etc. 114 Prefix/Suffix.
104 115
105 TODO(iannucci): combine nest_level and name_prefix 116 TODO(iannucci): combine nest_level and name_prefix
106 117
107 Example: 118 Example:
108 # suppose the OS's envar $OTHER is set to "yes" 119 # suppose the OS's envar $OTHER is set to "yes"
iannucci 2017/06/07 23:17:59 can we update this Example?
dnj 2017/06/07 23:48:46 Done.
109 with api.context(env={'ENV_VAR': 'something:%(OTHER)s'}): 120 with api.context(env={'ENV_VAR': 'something:%(OTHER)s'}):
110 # environment updates are additive. 121 # environment updates are additive.
111 with api.context(env={'OTHER': 'cool:%(OTHER)s'}): 122 with api.context(env={'OTHER': 'cool:%(OTHER)s'}):
112 # echos 'something:yes' 123 # echos 'something:yes'
113 # Note that the substitution always happens with the system 124 # Note that the substitution always happens with the system
114 # environment, not any of the computed environment here. 125 # environment, not any of the computed environment here.
115 api.step("check $ENV_VAR", ['bash', '-c', 'echo $ENV_VAR']) 126 api.step("check $ENV_VAR", ['bash', '-c', 'echo $ENV_VAR'])
116 # echos 'cool:yes' 127 # echos 'cool:yes'
117 api.step("check $OTHER", ['bash', '-c', 'echo $OTHER']) 128 api.step("check $OTHER", ['bash', '-c', 'echo $OTHER'])
118 129
(...skipping 28 matching lines...) Expand all
147 else: 158 else:
148 self._name_prefix.append(name_prefix) 159 self._name_prefix.append(name_prefix)
149 to_pop.append(self._name_prefix) 160 to_pop.append(self._name_prefix)
150 161
151 if env is not None and env != {}: 162 if env is not None and env != {}:
152 check_type('env', env, dict) 163 check_type('env', env, dict)
153 # we hit _env directly to avoid an extra copy. 164 # we hit _env directly to avoid an extra copy.
154 new = dict(self._env[-1]) 165 new = dict(self._env[-1])
155 for k, v in env.iteritems(): 166 for k, v in env.iteritems():
156 k = str(k) 167 k = str(k)
168 ev = new.get(k)
169 if ev is None:
170 ev = _EnvValue(str=None, prefixes=(), suffixes=())
157 if v is not None: 171 if v is not None:
158 v = str(v) 172 if isinstance(v, _EnvPathComponent):
159 try: 173 def _uniquify(*path_tuples):
160 # This odd little piece of code does the following: 174 seen = set()
161 # * add a bogus dictionary format %(foo)s to v. This forces % into 175 for path_tuple in path_tuples:
162 # 'dictionary lookup' mode 176 for path in path_tuple:
163 # * format the result with a defaultdict. This allows all 177 if path not in seen:
164 # `%(key)s` format lookups to succeed, but any sequential `%s` 178 seen.add(path)
165 # lookups to fail. 179 yield path
166 # If the string contains any accidental sequential lookups, this 180
167 # will raise an exception. If not, then this is a pluasible format 181 if v.is_prefix:
168 # string. 182 ev = ev._replace(prefixes=tuple(_uniquify(v.paths, ev.prefixes)))
iannucci 2017/06/07 23:17:59 let's just make it append
dnj 2017/06/07 23:48:47 Done.
169 ('%(foo)s'+v) % collections.defaultdict(str) 183 else:
170 except Exception: 184 ev = ev._replace(suffixes=tuple(_uniquify(ev.suffixes, v.paths)))
171 raise ValueError(('Invalid %%-formatting parameter in envvar, ' 185 else:
172 'only %%(ENVVAR)s allowed: %r') % (v,)) 186 v = str(v)
173 new[k] = v 187 try:
188 # This odd little piece of code does the following:
189 # * add a bogus dictionary format %(foo)s to v. This forces %
190 # into 'dictionary lookup' mode
191 # * format the result with a defaultdict. This allows all
192 # `%(key)s` format lookups to succeed, but any sequential `%s`
193 # lookups to fail.
194 # If the string contains any accidental sequential lookups, this
195 # will raise an exception. If not, then this is a pluasible format
196 # string.
197 ('%(foo)s'+v) % collections.defaultdict(str)
198 except Exception:
199 raise ValueError(('Invalid %%-formatting parameter in envvar, '
200 'only %%(ENVVAR)s allowed: %r') % (v,))
201 ev = ev._replace(str=v)
202 new[k] = ev
174 self._env.append(new) 203 self._env.append(new)
175 to_pop.append(self._env) 204 to_pop.append(self._env)
176 205
177 try: 206 try:
178 yield 207 yield
179 finally: 208 finally:
180 for p in to_pop: 209 for p in to_pop:
181 p.pop() 210 p.pop()
182 211
183 @property 212 @property
(...skipping 12 matching lines...) Expand all
196 225
197 By default this is empty; There's no facility to observe the program's 226 By default this is empty; There's no facility to observe the program's
198 startup environment. If you want to pass data to the recipe, it should be 227 startup environment. If you want to pass data to the recipe, it should be
199 done with properties. 228 done with properties.
200 229
201 Returns (dict) - The env-key -> value mapping of current environment 230 Returns (dict) - The env-key -> value mapping of current environment
202 modifications. 231 modifications.
203 """ 232 """
204 # TODO(iannucci): store env in an immutable way to avoid excessive copies. 233 # TODO(iannucci): store env in an immutable way to avoid excessive copies.
205 # TODO(iannucci): handle case-insensitive keys on windows 234 # TODO(iannucci): handle case-insensitive keys on windows
206 return dict(self._env[-1]) 235 def parts(ev):
236 for p in ev.prefixes:
237 yield str(p)
238 if ev.str is not None:
239 yield ev.str
240 for p in ev.suffixes:
241 yield str(p)
242
243 return {k: self.m.path.pathsep.join(parts(ev))
244 for k, ev in self._env[-1].iteritems()}
207 245
208 @property 246 @property
209 def infra_step(self): 247 def infra_step(self):
210 """Returns the current value of the infra_step setting. 248 """Returns the current value of the infra_step setting.
211 249
212 Returns (bool) - True iff steps are currently considered infra steps. 250 Returns (bool) - True iff steps are currently considered infra steps.
213 """ 251 """
214 return self._infra_step[-1] 252 return self._infra_step[-1]
215 253
216 @property 254 @property
217 def name_prefix(self): 255 def name_prefix(self):
218 """Gets the current step name prefix. 256 """Gets the current step name prefix.
219 257
220 Returns (str) - The string prefix that every step will have prepended to it. 258 Returns (str) - The string prefix that every step will have prepended to it.
221 """ 259 """
222 return self._name_prefix[-1] 260 return self._name_prefix[-1]
223 261
224 @property 262 @property
225 def nest_level(self): 263 def nest_level(self):
226 """Returns the current 'nesting' level. 264 """Returns the current 'nesting' level.
227 265
228 Note: This api is low-level, and you should always prefer to use 266 Note: This api is low-level, and you should always prefer to use
229 `api.step.nest`. This api is included for completeness and documentation 267 `api.step.nest`. This api is included for completeness and documentation
230 purposes. 268 purposes.
231 269
232 Returns (int) - The current nesting level. 270 Returns (int) - The current nesting level.
233 """ 271 """
234 return self._nest_level[-1] 272 return self._nest_level[-1]
273
274 def Prefix(self, *paths):
275 """Returns: an assignable "env" value that prefixes the specified paths to
276 the beginning of an environment variable.
277
278 Each path in paths is added, in order, as a prefix to the environment
279 variable, delimited by the OS path separator. This can be used for
280 easy manipulation of path environment variables such as PATH and PYTHONPATH.
281
282 Args:
283 paths (...Path): The list of paths to prefix.
284 """
285 for i, path in enumerate(paths):
286 check_type('path element %d' % (i,), path, Path)
287 return _EnvPathComponent(paths=paths, is_prefix=True)
288
289 def Suffix(self, *paths):
290 """Returns: an assignable "env" value that appends the specified paths to
291 the beginning of an environment variable.
292
293 Each path in paths is added, in order, as a suffix to the environment
294 variable, delimited by the OS path separator. This can be used for
295 easy manipulation of path environment variables such as PATH and PYTHONPATH.
296
297 Args:
298 paths (...Path): The list of paths to suffix.
299 """
300 for i, path in enumerate(paths):
301 check_type('path element %d' % (i,), path, Path)
302 return _EnvPathComponent(paths=paths, is_prefix=False)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698