Chromium Code Reviews| OLD | NEW |
|---|---|
| 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 Loading... | |
| 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 | |
| OLD | NEW |