| 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',)) |
| 47 |
| 48 _EnvValue = collections.namedtuple('_EnvValue', ( |
| 49 'prefixes', 'str')) |
| 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 one of: |
| 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 to attach a specific component to a |
| 111 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 Look at the examples in "examples/" for examples of context module usage. |
| 108 # suppose the OS's envar $OTHER is set to "yes" | |
| 109 with api.context(env={'ENV_VAR': 'something:%(OTHER)s'}): | |
| 110 # environment updates are additive. | |
| 111 with api.context(env={'OTHER': 'cool:%(OTHER)s'}): | |
| 112 # echos 'something:yes' | |
| 113 # Note that the substitution always happens with the system | |
| 114 # environment, not any of the computed environment here. | |
| 115 api.step("check $ENV_VAR", ['bash', '-c', 'echo $ENV_VAR']) | |
| 116 # echos 'cool:yes' | |
| 117 api.step("check $OTHER", ['bash', '-c', 'echo $OTHER']) | |
| 118 | |
| 119 with api.context(env={'OTHER': None}): | |
| 120 # echos '' | |
| 121 api.step("check $OTHER", ['bash', '-c', 'echo $OTHER']) | |
| 122 """ | 119 """ |
| 123 to_pop = [] | 120 to_pop = [] |
| 124 | 121 |
| 125 if cwd is not None: | 122 if cwd is not None: |
| 126 check_type('cwd', cwd, Path) | 123 check_type('cwd', cwd, Path) |
| 127 self._cwd.append(cwd) | 124 self._cwd.append(cwd) |
| 128 to_pop.append(self._cwd) | 125 to_pop.append(self._cwd) |
| 129 | 126 |
| 130 if infra_steps is not None: | 127 if infra_steps is not None: |
| 131 check_type('infra_steps', infra_steps, bool) | 128 check_type('infra_steps', infra_steps, bool) |
| (...skipping 15 matching lines...) Expand all Loading... |
| 147 else: | 144 else: |
| 148 self._name_prefix.append(name_prefix) | 145 self._name_prefix.append(name_prefix) |
| 149 to_pop.append(self._name_prefix) | 146 to_pop.append(self._name_prefix) |
| 150 | 147 |
| 151 if env is not None and env != {}: | 148 if env is not None and env != {}: |
| 152 check_type('env', env, dict) | 149 check_type('env', env, dict) |
| 153 # we hit _env directly to avoid an extra copy. | 150 # we hit _env directly to avoid an extra copy. |
| 154 new = dict(self._env[-1]) | 151 new = dict(self._env[-1]) |
| 155 for k, v in env.iteritems(): | 152 for k, v in env.iteritems(): |
| 156 k = str(k) | 153 k = str(k) |
| 154 ev = new.get(k) |
| 155 if ev is None: |
| 156 ev = _EnvValue(prefixes=(), str=None) |
| 157 if v is not None: | 157 if v is not None: |
| 158 v = str(v) | 158 if isinstance(v, _EnvPathComponent): |
| 159 try: | 159 ev = ev._replace(prefixes=v.paths+ev.prefixes) |
| 160 # This odd little piece of code does the following: | 160 else: |
| 161 # * add a bogus dictionary format %(foo)s to v. This forces % into | 161 v = str(v) |
| 162 # 'dictionary lookup' mode | 162 try: |
| 163 # * format the result with a defaultdict. This allows all | 163 # This odd little piece of code does the following: |
| 164 # `%(key)s` format lookups to succeed, but any sequential `%s` | 164 # * add a bogus dictionary format %(foo)s to v. This forces % |
| 165 # lookups to fail. | 165 # into 'dictionary lookup' mode |
| 166 # If the string contains any accidental sequential lookups, this | 166 # * format the result with a defaultdict. This allows all |
| 167 # will raise an exception. If not, then this is a pluasible format | 167 # `%(key)s` format lookups to succeed, but any sequential `%s` |
| 168 # string. | 168 # lookups to fail. |
| 169 ('%(foo)s'+v) % collections.defaultdict(str) | 169 # If the string contains any accidental sequential lookups, this |
| 170 except Exception: | 170 # will raise an exception. If not, then this is a pluasible format |
| 171 raise ValueError(('Invalid %%-formatting parameter in envvar, ' | 171 # string. |
| 172 'only %%(ENVVAR)s allowed: %r') % (v,)) | 172 ('%(foo)s'+v) % collections.defaultdict(str) |
| 173 new[k] = v | 173 except Exception: |
| 174 raise ValueError(('Invalid %%-formatting parameter in envvar, ' |
| 175 'only %%(ENVVAR)s allowed: %r') % (v,)) |
| 176 ev = ev._replace(str=v) |
| 177 new[k] = ev |
| 174 self._env.append(new) | 178 self._env.append(new) |
| 175 to_pop.append(self._env) | 179 to_pop.append(self._env) |
| 176 | 180 |
| 177 try: | 181 try: |
| 178 yield | 182 yield |
| 179 finally: | 183 finally: |
| 180 for p in to_pop: | 184 for p in to_pop: |
| 181 p.pop() | 185 p.pop() |
| 182 | 186 |
| 183 @property | 187 @property |
| (...skipping 12 matching lines...) Expand all Loading... |
| 196 | 200 |
| 197 By default this is empty; There's no facility to observe the program's | 201 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 | 202 startup environment. If you want to pass data to the recipe, it should be |
| 199 done with properties. | 203 done with properties. |
| 200 | 204 |
| 201 Returns (dict) - The env-key -> value mapping of current environment | 205 Returns (dict) - The env-key -> value mapping of current environment |
| 202 modifications. | 206 modifications. |
| 203 """ | 207 """ |
| 204 # TODO(iannucci): store env in an immutable way to avoid excessive copies. | 208 # TODO(iannucci): store env in an immutable way to avoid excessive copies. |
| 205 # TODO(iannucci): handle case-insensitive keys on windows | 209 # TODO(iannucci): handle case-insensitive keys on windows |
| 206 return dict(self._env[-1]) | 210 def parts(ev): |
| 211 for p in ev.prefixes: |
| 212 yield str(p) |
| 213 if ev.str is not None: |
| 214 yield ev.str |
| 215 |
| 216 return {k: self.m.path.pathsep.join(parts(ev)) |
| 217 for k, ev in self._env[-1].iteritems()} |
| 207 | 218 |
| 208 @property | 219 @property |
| 209 def infra_step(self): | 220 def infra_step(self): |
| 210 """Returns the current value of the infra_step setting. | 221 """Returns the current value of the infra_step setting. |
| 211 | 222 |
| 212 Returns (bool) - True iff steps are currently considered infra steps. | 223 Returns (bool) - True iff steps are currently considered infra steps. |
| 213 """ | 224 """ |
| 214 return self._infra_step[-1] | 225 return self._infra_step[-1] |
| 215 | 226 |
| 216 @property | 227 @property |
| 217 def name_prefix(self): | 228 def name_prefix(self): |
| 218 """Gets the current step name prefix. | 229 """Gets the current step name prefix. |
| 219 | 230 |
| 220 Returns (str) - The string prefix that every step will have prepended to it. | 231 Returns (str) - The string prefix that every step will have prepended to it. |
| 221 """ | 232 """ |
| 222 return self._name_prefix[-1] | 233 return self._name_prefix[-1] |
| 223 | 234 |
| 224 @property | 235 @property |
| 225 def nest_level(self): | 236 def nest_level(self): |
| 226 """Returns the current 'nesting' level. | 237 """Returns the current 'nesting' level. |
| 227 | 238 |
| 228 Note: This api is low-level, and you should always prefer to use | 239 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 | 240 `api.step.nest`. This api is included for completeness and documentation |
| 230 purposes. | 241 purposes. |
| 231 | 242 |
| 232 Returns (int) - The current nesting level. | 243 Returns (int) - The current nesting level. |
| 233 """ | 244 """ |
| 234 return self._nest_level[-1] | 245 return self._nest_level[-1] |
| 246 |
| 247 def Prefix(self, *paths): |
| 248 """Returns: an assignable "env" value that prefixes the specified paths to |
| 249 the beginning of an environment variable. |
| 250 |
| 251 Each path in paths is added, in order, as a prefix to the environment |
| 252 variable, delimited by the OS path separator. This can be used for |
| 253 easy manipulation of path environment variables such as PATH and PYTHONPATH. |
| 254 |
| 255 Args: |
| 256 paths (...Path): The list of paths to prefix. |
| 257 """ |
| 258 for i, path in enumerate(paths): |
| 259 check_type('path element %d' % (i,), path, Path) |
| 260 return _EnvPathComponent(paths=paths) |
| OLD | NEW |