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. |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 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 |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 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 |
| 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 _augment(st, val): | |
|
iannucci
2017/06/12 19:58:40
_push
?
dnj
2017/06/13 19:31:21
Done.
| |
| 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 _augment(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 _augment(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 _augment(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 _augment(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 | |
| 149 new[k] = tuple(v) + new.get(k, ()) | |
| 150 _augment(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 _augment(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 Loading... | |
| 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) | |
| OLD | NEW |