| 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 |
| 96 with api.context(name_prefix='world'): | 90 with api.context(name_prefix='world'): |
| 97 # has name 'hello.world.other' | 91 # has name 'hello.world.other' |
| 98 api.step('other', ['echo', 'other']) | 92 api.step('other', ['echo', 'other']) |
| 99 | 93 |
| 100 Environmental Variable Overrides: | 94 Environmental Variable Overrides: |
| 101 | 95 |
| 102 Env is a mapping of environment variable name to the value you want that | 96 Env is a mapping of environment variable name to the value you want that |
| 103 environment variable to have. The value is one of: | 97 environment variable to have. The value is one of: |
| 104 * None, indicating that the environment variable should be removed from | 98 * None, indicating that the environment variable should be removed from |
| 105 the environment when the step runs. | 99 the environment when the step runs. |
| 106 * A string value. Note that string values will be %-formatted with the | 100 * A string value. Note that string values will be %-formatted with the |
| 107 current value of the environment at the time the step runs. This means | 101 current value of the environment at the time the step runs. This means |
| 108 that you can have a value like: | 102 that you can have a value like: |
| 109 "/path/to/my/stuff:%(PATH)s" | 103 "/path/to/my/stuff:%(PATH)s" |
| 110 Which, at the time the step executes, will inject the current value of | 104 Which, at the time the step executes, will inject the current value of |
| 111 $PATH. | 105 $PATH. |
| 112 * A sentinel value such as Prefix to attach a specific component to a | |
| 113 pathsep-delimited list variable. | |
| 114 | 106 |
| 115 TODO(iannucci,dnj): Disallow "env" values to be mixes of string or | 107 "env_prefix" is a list of Path or strings that get prefixed to their |
| 116 Prefix/Suffix. | 108 respective environment variables, delimited with the system's path |
| 109 separator. This can be used to add entries to environment variables such |
| 110 as "PATH" and "PYTHONPATH". If prefixes are specified and a value is also |
| 111 defined in "env", it will be installed as the last path component if it is |
| 112 not empty. |
| 117 | 113 |
| 118 TODO(iannucci): combine nest_level and name_prefix | 114 TODO(iannucci): combine nest_level and name_prefix |
| 119 | 115 |
| 120 Look at the examples in "examples/" for examples of context module usage. | 116 Look at the examples in "examples/" for examples of context module usage. |
| 121 """ | 117 """ |
| 122 to_pop = [] | 118 to_pop = [] |
| 119 def _push(st, val): |
| 120 st.append(val) |
| 121 to_pop.append(st) |
| 123 | 122 |
| 124 if cwd is not None: | 123 if cwd is not None: |
| 125 check_type('cwd', cwd, Path) | 124 check_type('cwd', cwd, Path) |
| 126 self._cwd.append(cwd) | 125 _push(self._cwd, cwd) |
| 127 to_pop.append(self._cwd) | |
| 128 | 126 |
| 129 if infra_steps is not None: | 127 if infra_steps is not None: |
| 130 check_type('infra_steps', infra_steps, bool) | 128 check_type('infra_steps', infra_steps, bool) |
| 131 self._infra_step.append(infra_steps) | 129 _push(self._infra_step, infra_steps) |
| 132 to_pop.append(self._infra_step) | |
| 133 | 130 |
| 134 if increment_nest_level is not None: | 131 if increment_nest_level is not None: |
| 135 check_type('increment_nest_level', increment_nest_level, bool) | 132 check_type('increment_nest_level', increment_nest_level, bool) |
| 136 if not increment_nest_level: | 133 if not increment_nest_level: |
| 137 raise ValueError('increment_nest_level=False makes no sense') | 134 raise ValueError('increment_nest_level=False makes no sense') |
| 138 self._nest_level.append(self.nest_level+1) | 135 _push(self._nest_level, self.nest_level+1) |
| 139 to_pop.append(self._nest_level) | |
| 140 | 136 |
| 141 if name_prefix is not None: | 137 if name_prefix is not None: |
| 142 check_type('name_prefix', name_prefix, str) | 138 check_type('name_prefix', name_prefix, str) |
| 143 cur = self.name_prefix | 139 cur = self.name_prefix |
| 144 if cur: | 140 if cur: |
| 145 self._name_prefix.append('%s.%s' % (cur, name_prefix)) | 141 name_prefix = '%s.%s' % (cur, name_prefix) |
| 146 else: | 142 _push(self._name_prefix, name_prefix) |
| 147 self._name_prefix.append(name_prefix) | |
| 148 to_pop.append(self._name_prefix) | |
| 149 | 143 |
| 150 if env is not None and env != {}: | 144 if env_prefixes is not None and len(env_prefixes) > 0: |
| 145 check_type('env_prefixes', env_prefixes, dict) |
| 146 new = dict(self._env_prefixes[-1]) |
| 147 for k, v in env_prefixes.iteritems(): |
| 148 if not v: |
| 149 continue |
| 150 k = str(k) |
| 151 new[k] = tuple(v) + new.get(k, ()) |
| 152 _push(self._env_prefixes, new) |
| 153 |
| 154 if env is not None and len(env) > 0: |
| 151 check_type('env', env, dict) | 155 check_type('env', env, dict) |
| 152 # we hit _env directly to avoid an extra copy. | 156 # we hit _env directly to avoid an extra copy. |
| 153 new = dict(self._env[-1]) | 157 new = dict(self._env[-1]) |
| 154 for k, v in env.iteritems(): | 158 for k, v in env.iteritems(): |
| 155 k = str(k) | 159 k = str(k) |
| 156 if v is None: | 160 if v is not None: |
| 157 ev = _EnvValue(prefixes=(), value=None) | 161 v = str(v) |
| 158 else: | 162 try: |
| 159 ev = new.get(k, _EnvValue(prefixes=(), value='')) | 163 # This odd little piece of code does the following: |
| 160 if isinstance(v, _EnvPathComponent): | 164 # * add a bogus dictionary format %(foo)s to v. This forces % |
| 161 ev = ev._replace(prefixes=v.paths+ev.prefixes) | 165 # into 'dictionary lookup' mode |
| 162 else: | 166 # * format the result with a defaultdict. This allows all |
| 163 v = str(v) | 167 # `%(key)s` format lookups to succeed, but any sequential `%s` |
| 164 try: | 168 # lookups to fail. |
| 165 # This odd little piece of code does the following: | 169 # If the string contains any accidental sequential lookups, this |
| 166 # * add a bogus dictionary format %(foo)s to v. This forces % | 170 # will raise an exception. If not, then this is a pluasible format |
| 167 # into 'dictionary lookup' mode | 171 # string. |
| 168 # * format the result with a defaultdict. This allows all | 172 ('%(foo)s'+v) % collections.defaultdict(str) |
| 169 # `%(key)s` format lookups to succeed, but any sequential `%s` | 173 except Exception: |
| 170 # lookups to fail. | 174 raise ValueError(('Invalid %%-formatting parameter in envvar, ' |
| 171 # If the string contains any accidental sequential lookups, this | 175 'only %%(ENVVAR)s allowed: %r') % (v,)) |
| 172 # will raise an exception. If not, then this is a pluasible format | 176 new[k] = v |
| 173 # string. | 177 _push(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 | 178 |
| 183 try: | 179 try: |
| 184 yield | 180 yield |
| 185 finally: | 181 finally: |
| 186 for p in to_pop: | 182 for p in to_pop: |
| 187 p.pop() | 183 p.pop() |
| 188 | 184 |
| 189 @property | 185 @property |
| 190 def cwd(self): | 186 def cwd(self): |
| 191 """Returns the current working directory that steps will run in. | 187 """Returns the current working directory that steps will run in. |
| (...skipping 10 matching lines...) Expand all Loading... |
| 202 | 198 |
| 203 By default this is empty; There's no facility to observe the program's | 199 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 | 200 startup environment. If you want to pass data to the recipe, it should be |
| 205 done with properties. | 201 done with properties. |
| 206 | 202 |
| 207 Returns (dict) - The env-key -> value mapping of current environment | 203 Returns (dict) - The env-key -> value mapping of current environment |
| 208 modifications. | 204 modifications. |
| 209 """ | 205 """ |
| 210 # TODO(iannucci): store env in an immutable way to avoid excessive copies. | 206 # TODO(iannucci): store env in an immutable way to avoid excessive copies. |
| 211 # TODO(iannucci): handle case-insensitive keys on windows | 207 # TODO(iannucci): handle case-insensitive keys on windows |
| 212 def parts(ev): | 208 return dict(self._env[-1]) |
| 213 for p in ev.prefixes: | |
| 214 yield str(p) | |
| 215 if ev.value: | |
| 216 yield ev.value | |
| 217 | 209 |
| 218 ret = {} | 210 @property |
| 219 for k, ev in self._env[-1].iteritems(): | 211 def env_prefixes(self): |
| 220 if ev.value is None: | 212 """Returns Path prefix modifications to the environment. |
| 221 ret[k] = None | |
| 222 else: | |
| 223 ret[k] = self.m.path.pathsep.join(parts(ev)) | |
| 224 | 213 |
| 225 return ret | 214 This will return a mapping of environment key to Path tuple for Path |
| 215 prefixes registered with the environment. |
| 216 |
| 217 Returns (dict) - The env-key -> value(Path) mapping of current environment |
| 218 prefix modifications. |
| 219 """ |
| 220 # TODO(iannucci): store env in an immutable way to avoid excessive copies. |
| 221 # TODO(iannucci): handle case-insensitive keys on windows |
| 222 return dict(self._env_prefixes[-1]) |
| 226 | 223 |
| 227 @property | 224 @property |
| 228 def infra_step(self): | 225 def infra_step(self): |
| 229 """Returns the current value of the infra_step setting. | 226 """Returns the current value of the infra_step setting. |
| 230 | 227 |
| 231 Returns (bool) - True iff steps are currently considered infra steps. | 228 Returns (bool) - True iff steps are currently considered infra steps. |
| 232 """ | 229 """ |
| 233 return self._infra_step[-1] | 230 return self._infra_step[-1] |
| 234 | 231 |
| 235 @property | 232 @property |
| 236 def name_prefix(self): | 233 def name_prefix(self): |
| 237 """Gets the current step name prefix. | 234 """Gets the current step name prefix. |
| 238 | 235 |
| 239 Returns (str) - The string prefix that every step will have prepended to it. | 236 Returns (str) - The string prefix that every step will have prepended to it. |
| 240 """ | 237 """ |
| 241 return self._name_prefix[-1] | 238 return self._name_prefix[-1] |
| 242 | 239 |
| 243 @property | 240 @property |
| 244 def nest_level(self): | 241 def nest_level(self): |
| 245 """Returns the current 'nesting' level. | 242 """Returns the current 'nesting' level. |
| 246 | 243 |
| 247 Note: This api is low-level, and you should always prefer to use | 244 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 | 245 `api.step.nest`. This api is included for completeness and documentation |
| 249 purposes. | 246 purposes. |
| 250 | 247 |
| 251 Returns (int) - The current nesting level. | 248 Returns (int) - The current nesting level. |
| 252 """ | 249 """ |
| 253 return self._nest_level[-1] | 250 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 |