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

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

Issue 2925453002: [context] Add path prefix/suffix manipulation. (Closed)
Patch Set: advise 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
29 30
30 from contextlib import contextmanager 31 from contextlib import contextmanager
31 32
32 from recipe_engine import recipe_api 33 from recipe_engine import recipe_api
33 from recipe_engine.config_types import Path 34 from recipe_engine.config_types import Path
34 from recipe_engine.recipe_api import RecipeApi 35 from recipe_engine.recipe_api import RecipeApi
35 36
36 37
37 def check_type(name, var, expect): 38 def check_type(name, var, expect):
38 if not isinstance(var, expect): # pragma: no cover 39 if not isinstance(var, expect): # pragma: no cover
39 raise TypeError('%s is not %s: %r (%s)' % ( 40 raise TypeError('%s is not %s: %r (%s)' % (
40 name, expect.__name__, var, type(var).__name__)) 41 name, expect.__name__, var, type(var).__name__))
41 42
42 43
43 class ContextApi(RecipeApi): 44 class ContextApi(RecipeApi):
45
44 # TODO(iannucci): move implementation of these data directly into this class. 46 # TODO(iannucci): move implementation of these data directly into this class.
45 def __init__(self, **kwargs): 47 def __init__(self, **kwargs):
46 super(RecipeApi, self).__init__(**kwargs) 48 super(RecipeApi, self).__init__(**kwargs)
47 49
48 self._cwd = [None] 50 self._cwd = [None]
49 self._env = [{}] 51 self._env = [{}]
50 self._infra_step = [False] 52 self._infra_step = [False]
51 self._name_prefix = [''] 53 self._name_prefix = ['']
52 # this could be a number, but it makes the logic easier to use a stack. 54 # this could be a number, but it makes the logic easier to use a stack.
53 self._nest_level = [0] 55 self._nest_level = [0]
56 self._prefix_paths = [{}]
57 self._suffix_paths = [{}]
54 58
55 @contextmanager 59 @contextmanager
56 def __call__(self, cwd=None, env=None, increment_nest_level=None, 60 def __call__(self, cwd=None, env=None, increment_nest_level=None,
57 infra_steps=None, name_prefix=None): 61 infra_steps=None, name_prefix=None, path_prefix=None,
62 path_suffix=None):
58 """Allows adjustment of multiple context values in a single call. 63 """Allows adjustment of multiple context values in a single call.
59 64
60 Contextual data: 65 Contextual data:
61 * cwd (Path) - the current working directory to use for all steps. 66 * cwd (Path) - the current working directory to use for all steps.
62 To 'reset' to the original cwd at the time recipes started, pass 67 To 'reset' to the original cwd at the time recipes started, pass
63 `api.path['start_dir']`. 68 `api.path['start_dir']`.
64 * infra_steps (bool) - if steps in this context should be considered 69 * infra_steps (bool) - if steps in this context should be considered
65 infrastructure steps. On failure, these will raise InfraFailure 70 infrastructure steps. On failure, these will raise InfraFailure
66 exceptions instead of StepFailure exceptions. 71 exceptions instead of StepFailure exceptions.
67 * increment_nest_level (True) - increment the nest level by 1 in this 72 * increment_nest_level (True) - increment the nest level by 1 in this
68 context. Typically you won't directly interact with this, but should 73 context. Typically you won't directly interact with this, but should
69 use api.step.nest instead. 74 use api.step.nest instead.
70 * name_prefix (str) - A string to prepend to the names of all steps in 75 * name_prefix (str) - A string to prepend to the names of all steps in
71 this context. These compose with '.' characters if multiple name prefix 76 this context. These compose with '.' characters if multiple name prefix
72 contexts occur. See below for more info. 77 contexts occur. See below for more info.
73 * env (dict) - Environmental variable overrides. See below for more info. 78 * env (dict) - Environmental variable overrides. See below for more info.
iannucci 2017/06/05 19:23:04 wdyt about something like: with api.context(env
dnj 2017/06/07 02:59:29 That seems fine. For now I think we need mixed-use
79 * path_prefix (dict) - Environmental variable prefix path overrides.
80 Consider using the "path_prefix" and "path_remove" helper functions
81 instead of directly manipulating this value.
82 * path_suffix (dict) - Environmental variable suffix path overrides.
83 Consider using the "path_suffix" and "path_remove" helper functions
84 instead of directly manipulating this value.
iannucci 2017/06/05 19:23:04 I don't like the helper function idea; we should k
dnj 2017/06/07 02:59:29 SGTM
74 85
75 Name prefixes: 86 Name prefixes:
76 87
77 Multiple invocations concatenate values with '.'. 88 Multiple invocations concatenate values with '.'.
78 89
79 Example: 90 Example:
80 with api.context(name_prefix='hello'): 91 with api.context(name_prefix='hello'):
81 # has name 'hello.something' 92 # has name 'hello.something'
82 api.step('something', ['echo', 'something']) 93 api.step('something', ['echo', 'something'])
83 94
84 with api.context(name_prefix='world'): 95 with api.context(name_prefix='world'):
85 # has name 'hello.world.other' 96 # has name 'hello.world.other'
86 api.step('other', ['echo', 'other']) 97 api.step('other', ['echo', 'other'])
87 98
88 Environmental Variable Overrides: 99 Environmental Variable Overrides:
89 100
90 Env is a mapping of environment variable name to the value you want that 101 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 102 environment variable to have. The value is a string, with a couple
92 exceptions: 103 exceptions:
93 * If value is None, this environment variable will be removed from the 104 * If value is None, this environment variable will be removed from the
94 environment when the step runs. 105 environment when the step runs.
95 * String values will be %-formatted with the current value of the 106 * String values will be %-formatted with the current value of the
96 environment at the time the step runs. This means that you can have 107 environment at the time the step runs. This means that you can have
97 a value like: 108 a value like:
98 "/path/to/my/stuff:%(PATH)s" 109 "/path/to/my/stuff:%(PATH)s"
99 Which, at the time the step executes, will inject the current value of 110 Which, at the time the step executes, will inject the current value of
100 $PATH. 111 $PATH.
101 112
102 TODO(iannucci): implement env_paths which allows for easier manipulation of 113 path_prefix and path_suffix are mappings of environment variables to lists
103 `pathsep` environment variables like $PATH, $PYTHONPATH, etc. 114 of Path elements, meant to be applied to "pathsep"-separated environment
115 variables such as PATH and PYTHONPATH. These values are prefixed and/or
116 appended to their environment variable key, separated by the OS-specific
117 path separator.
118 * If key begins with a "!" (e.g., "!PATH"), the named element will be
119 removed from its environment instead of added.
120 * Consider using helper functions instead of directly manipulating this
121 value.
104 122
105 TODO(iannucci): combine nest_level and name_prefix 123 TODO(iannucci): combine nest_level and name_prefix
106 124
107 Example: 125 Example:
108 # suppose the OS's envar $OTHER is set to "yes" 126 # suppose the OS's envar $OTHER is set to "yes"
109 with api.context(env={'ENV_VAR': 'something:%(OTHER)s'}): 127 with api.context(env={'ENV_VAR': 'something:%(OTHER)s'}):
110 # environment updates are additive. 128 # environment updates are additive.
111 with api.context(env={'OTHER': 'cool:%(OTHER)s'}): 129 with api.context(env={'OTHER': 'cool:%(OTHER)s'}):
112 # echos 'something:yes' 130 # echos 'something:yes'
113 # Note that the substitution always happens with the system 131 # Note that the substitution always happens with the system
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
167 # will raise an exception. If not, then this is a pluasible format 185 # will raise an exception. If not, then this is a pluasible format
168 # string. 186 # string.
169 ('%(foo)s'+v) % collections.defaultdict(str) 187 ('%(foo)s'+v) % collections.defaultdict(str)
170 except Exception: 188 except Exception:
171 raise ValueError(('Invalid %%-formatting parameter in envvar, ' 189 raise ValueError(('Invalid %%-formatting parameter in envvar, '
172 'only %%(ENVVAR)s allowed: %r') % (v,)) 190 'only %%(ENVVAR)s allowed: %r') % (v,))
173 new[k] = v 191 new[k] = v
174 self._env.append(new) 192 self._env.append(new)
175 to_pop.append(self._env) 193 to_pop.append(self._env)
176 194
195 for var, collection, name, is_prefix in (
196 (path_prefix, self._prefix_paths, 'path_prefix', True),
197 (path_suffix, self._suffix_paths, 'path_suffix', False)):
198 if var is None or var == {}:
199 continue
200 check_type(name, var, dict)
201 new = dict(collection[-1])
202 for k, paths in var.iteritems():
203 check_type(k, paths, collections.Iterable)
iannucci 2017/06/05 19:23:04 check_type("KEY %s[%r]" % (name, k), ...) so then
dnj 2017/06/07 02:59:28 No longer needed.
204 remove = k.startswith('!')
205 if remove:
206 k = k[1:]
iannucci 2017/06/05 19:23:04 let's remove the removal functionality for now
dnj 2017/06/07 02:59:29 Done.
207
208 # Remove any occurrences of the current value and any duplicates from
209 # "paths". This will keep the prefix/suffix lists clean.
210 nv = new.get(k, ())
211 seen = set()
212 unique_paths = []
213 for i, path in enumerate(paths):
214 check_type('%s element #%d' % (k, i), path, Path)
iannucci 2017/06/05 19:23:04 '%s[%r]' % (name, k)
dnj 2017/06/07 02:59:28 Also no longer needed.
215 nv = tuple(x for x in nv if x != path)
216 if path not in seen:
217 unique_paths.append(path)
218 seen.add(path)
219 if not remove:
220 if is_prefix:
221 nv = tuple(unique_paths) + nv
222 else:
223 nv += tuple(unique_paths)
224 if nv:
225 new[k] = nv
226 else:
227 new.pop(k, None)
228 collection.append(new)
229 to_pop.append(collection)
230
177 try: 231 try:
178 yield 232 yield
179 finally: 233 finally:
180 for p in to_pop: 234 for p in to_pop:
181 p.pop() 235 p.pop()
182 236
183 @property 237 @property
184 def cwd(self): 238 def cwd(self):
185 """Returns the current working directory that steps will run in. 239 """Returns the current working directory that steps will run in.
186 240
187 Returns (Path|None) - The current working directory. A value of None is 241 Returns (Path|None) - The current working directory. A value of None is
188 equivalent to api.path['start_dir'], though only occurs if no cwd has been 242 equivalent to api.path['start_dir'], though only occurs if no cwd has been
189 set (e.g. in the outermost context of RunSteps). 243 set (e.g. in the outermost context of RunSteps).
190 """ 244 """
191 return self._cwd[-1] 245 return self._cwd[-1]
192 246
193 @property 247 @property
194 def env(self): 248 def env(self):
195 """Returns modifications to the environment. 249 """Returns modifications to the environment.
196 250
197 By default this is empty; There's no facility to observe the program's 251 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 252 startup environment. If you want to pass data to the recipe, it should be
199 done with properties. 253 done with properties.
200 254
201 Returns (dict) - The env-key -> value mapping of current environment 255 Returns (dict) - The env-key -> value mapping of current environment
202 modifications. 256 modifications.
203 """ 257 """
204 # TODO(iannucci): store env in an immutable way to avoid excessive copies. 258 # TODO(iannucci): store env in an immutable way to avoid excessive copies.
205 # TODO(iannucci): handle case-insensitive keys on windows 259 # TODO(iannucci): handle case-insensitive keys on windows
206 return dict(self._env[-1]) 260 env = dict(self._env[-1])
261 for collection, is_prefix in (
262 (self._prefix_paths, True),
263 (self._suffix_paths, False)):
264 for key, paths in collection[-1].iteritems():
265 cur = env.get(key, '%%(%s)s' % (key,))
266 cur = cur.split(os.pathsep) if cur else []
iannucci 2017/06/05 19:24:04 this will fail in production/simulation; use self.
dnj 2017/06/07 02:59:29 Done.
267 if is_prefix:
268 cur = list(str(p) for p in paths) + cur
269 else:
270 cur += list(str(p) for p in paths)
271 env[key] = os.pathsep.join(cur)
272 return env
iannucci 2017/06/05 19:23:04 I'm not sure how I feel about this; I think that s
dnj 2017/06/07 02:59:29 Done.
207 273
208 @property 274 @property
209 def infra_step(self): 275 def infra_step(self):
210 """Returns the current value of the infra_step setting. 276 """Returns the current value of the infra_step setting.
211 277
212 Returns (bool) - True iff steps are currently considered infra steps. 278 Returns (bool) - True iff steps are currently considered infra steps.
213 """ 279 """
214 return self._infra_step[-1] 280 return self._infra_step[-1]
215 281
216 @property 282 @property
217 def name_prefix(self): 283 def name_prefix(self):
218 """Gets the current step name prefix. 284 """Gets the current step name prefix.
219 285
220 Returns (str) - The string prefix that every step will have prepended to it. 286 Returns (str) - The string prefix that every step will have prepended to it.
221 """ 287 """
222 return self._name_prefix[-1] 288 return self._name_prefix[-1]
223 289
224 @property 290 @property
225 def nest_level(self): 291 def nest_level(self):
226 """Returns the current 'nesting' level. 292 """Returns the current 'nesting' level.
227 293
228 Note: This api is low-level, and you should always prefer to use 294 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 295 `api.step.nest`. This api is included for completeness and documentation
230 purposes. 296 purposes.
231 297
232 Returns (int) - The current nesting level. 298 Returns (int) - The current nesting level.
233 """ 299 """
234 return self._nest_level[-1] 300 return self._nest_level[-1]
301
302 @contextmanager
303 def path_prefix(self, key, *paths):
304 """Adds specified paths to the beginning of an environment variable.
305
306 Each path in paths is added, in order, as a prefix to the environment
307 variable "key", delimited by the OS path separator. This can be used for
308 easy manipulation of path environment variables such as PATH and PYTHONPATH.
309
310 If "key" begins with an "!", the specified paths will be removed from the
311 prefix. The "path_remove" helper function may be a more elegant way to
312 remove a path.
313
314 Args:
315 key (str): The environment variable key.
316 paths (...Path): The list of paths to prefix.
317 """
318 check_type('key', key, str)
319 with self(path_prefix={key: paths}):
320 yield
321
322 @contextmanager
323 def path_suffix(self, key, *paths):
324 """Adds specified paths to the end of an environment variable.
325
326 Each path in paths is added, in order, as a suffix to the environment
327 variable "key", delimited by the OS path separator. This can be used for
328 easy manipulation of path environment variables such as PATH and PYTHONPATH.
329
330 If "key" begins with an "!", the specified paths will be removed from the
331 prefix. The "path_remove" helper function may be a more elegant way to
332 remove a path.
333
334 Args:
335 key (str): The environment variable key.
336 paths (...Path): The list of paths to suffix.
337 """
338 check_type('key', key, str)
339 with self(path_suffix={key: paths}):
340 yield
341
342 @contextmanager
343 def path_remove(self, key, *paths):
iannucci 2017/06/05 19:23:04 let's remove this functionality for now. I can't t
dnj 2017/06/07 02:59:29 Done.
344 """Removes specified paths from an environment variable's prefix/suffix.
345
346 Any path in "paths" will be removed from the environment's prefix and suffix
347 paths. If the path is not present, it will be ignored.
348
349 WARNING: this will NOT remove a path that was not, itself, added as a
350 prefix or suffix. For example, if a path is added to the environment as a
351 string, rather than through "path_prefix" or "path_suffix", it will not be
352 removed.
353
354 Args:
355 key (str): The environment variable key.
356 paths (...Path): The list of paths to suffix.
357 """
358 check_type('key', key, str)
359 if not key.startswith('!'):
360 key = '!'+key
361 with self(path_prefix={key: paths}, path_suffix={key: paths}):
362 yield
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698