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 | |
| 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 Loading... | |
| 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 Loading... | |
| 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 Loading... | |
| 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) | |
| OLD | NEW |