| # Copyright 2017 The LUCI Authors. All rights reserved. |
| # Use of this source code is governed under the Apache License, Version 2.0 |
| # that can be found in the LICENSE file. |
| |
| """The context module provides APIs for manipulating a few pieces of 'ambient' |
| data that affect how steps are run. |
| |
| The pieces of information which can be modified are: |
| * cwd - The current working directory. |
| * env - The environment variables. |
| * infra_step - Whether or not failures should be treated as infrastructure |
| failures vs. normal failures. |
| * name_prefix - A prefix for all step names. |
| * nest_level - An indicator for the UI of how deeply to nest steps. |
| |
| The values here are all scoped using Python's `with` statement; there's no |
| mechanism to make an open-ended adjustment to these values (i.e. there's no way |
| to change the cwd permanently for a recipe, except by surrounding the entire |
| recipe with a with statement). This is done to avoid the surprises that |
| typically arise with things like os.environ or os.chdir in a normal python |
| program. |
| |
| Example: |
| ```python |
| with api.context(cwd=api.path['start_dir'].join('subdir')): |
| # this step is run inside of the subdir directory. |
| api.step("cat subdir/foo", ['cat', './foo']) |
| ``` |
| """ |
| |
| |
| import collections |
| import os |
| import types |
| |
| from contextlib import contextmanager |
| |
| from recipe_engine import recipe_api |
| from recipe_engine.config_types import Path |
| from recipe_engine.recipe_api import RecipeApi |
| |
| |
| def check_type(name, var, expect): |
| if not isinstance(var, expect): # pragma: no cover |
| raise TypeError('%s is not %s: %r (%s)' % ( |
| name, expect.__name__, var, type(var).__name__)) |
| |
| |
| class ContextApi(RecipeApi): |
| |
| # TODO(iannucci): move implementation of these data directly into this class. |
| def __init__(self, **kwargs): |
| super(RecipeApi, self).__init__(**kwargs) |
| |
| self._cwd = [None] |
| self._env_prefixes = [{}] |
| self._env_suffixes = [{}] |
| self._env = [{}] |
| self._infra_step = [False] |
| self._name_prefix = [''] |
| # this could be a number, but it makes the logic easier to use a stack. |
| self._nest_level = [0] |
| |
| @contextmanager |
| def __call__(self, cwd=None, env_prefixes=None, env_suffixes=None, env=None, |
| increment_nest_level=None, infra_steps=None, name_prefix=None): |
| """Allows adjustment of multiple context values in a single call. |
| |
| Args: |
| * cwd (Path) - the current working directory to use for all steps. |
| To 'reset' to the original cwd at the time recipes started, pass |
| `api.path['start_dir']`. |
| * env_prefixes (dict) - Environmental variable prefix augmentations. See |
| below for more info. |
| * env_suffixes (dict) - Environmental variable suffix augmentations. See |
| below for more info. |
| * env (dict) - Environmental variable overrides. See below for more info. |
| * increment_nest_level (True) - increment the nest level by 1 in this |
| context. Typically you won't directly interact with this, but should |
| use api.step.nest instead. |
| * infra_steps (bool) - if steps in this context should be considered |
| infrastructure steps. On failure, these will raise InfraFailure |
| exceptions instead of StepFailure exceptions. |
| * name_prefix (str) - A string to prepend to the names of all steps in |
| this context. These compose with '.' characters if multiple name prefix |
| contexts occur. See below for more info. |
| |
| Name prefixes: |
| |
| Multiple invocations concatenate values with '.'. |
| |
| Example: |
| ```python |
| with api.context(name_prefix='hello'): |
| # has name 'hello.something' |
| api.step('something', ['echo', 'something']) |
| |
| with api.context(name_prefix='world'): |
| # has name 'hello.world.other' |
| api.step('other', ['echo', 'other']) |
| ``` |
| |
| Environmental Variable Overrides: |
| |
| Env is a mapping of environment variable name to the value you want that |
| environment variable to have. The value is one of: |
| * None, indicating that the environment variable should be removed from |
| the environment when the step runs. |
| * A string value. Note that string values will be %-formatted with the |
| current value of the environment at the time the step runs. This means |
| that you can have a value like: |
| "/path/to/my/stuff:%(PATH)s" |
| Which, at the time the step executes, will inject the current value of |
| $PATH. |
| |
| "env_prefix" and "env_suffix" are a list of Path or strings that get |
| prefixed (or suffixed) to their respective environment variables, delimited |
| with the system's path separator. This can be used to add entries to |
| environment variables such as "PATH" and "PYTHONPATH". If prefixes are |
| specified and a value is also defined in "env", the value will be installed |
| as the last path component if it is not empty. |
| |
| **TODO(iannucci): combine nest_level and name_prefix** |
| |
| Look at the examples in "examples/" for examples of context module usage. |
| """ |
| to_pop = [] |
| def _push(st, val): |
| st.append(val) |
| to_pop.append(st) |
| |
| if cwd is not None: |
| check_type('cwd', cwd, Path) |
| _push(self._cwd, cwd) |
| |
| if infra_steps is not None: |
| check_type('infra_steps', infra_steps, bool) |
| _push(self._infra_step, infra_steps) |
| |
| if increment_nest_level is not None: |
| check_type('increment_nest_level', increment_nest_level, bool) |
| if not increment_nest_level: |
| raise ValueError('increment_nest_level=False makes no sense') |
| _push(self._nest_level, self.nest_level+1) |
| |
| if name_prefix is not None: |
| check_type('name_prefix', name_prefix, str) |
| cur = self.name_prefix |
| if cur: |
| name_prefix = '%s.%s' % (cur, name_prefix) |
| _push(self._name_prefix, name_prefix) |
| |
| if env_prefixes is not None and len(env_prefixes) > 0: |
| check_type('env_prefixes', env_prefixes, dict) |
| new = dict(self._env_prefixes[-1]) |
| for k, v in env_prefixes.iteritems(): |
| if not v: |
| continue |
| k = str(k) |
| new[k] = tuple(v) + new.get(k, ()) |
| _push(self._env_prefixes, new) |
| |
| if env_suffixes is not None and len(env_suffixes) > 0: |
| check_type('env_suffixes', env_suffixes, dict) |
| new = dict(self._env_suffixes[-1]) |
| for k, v in env_suffixes.iteritems(): |
| if not v: |
| continue |
| k = str(k) |
| new[k] = new.get(k, ()) + tuple(v) |
| _push(self._env_suffixes, new) |
| |
| if env is not None and len(env) > 0: |
| check_type('env', env, dict) |
| # we hit _env directly to avoid an extra copy. |
| new = dict(self._env[-1]) |
| for k, v in env.iteritems(): |
| k = str(k) |
| if v is not None: |
| v = str(v) |
| try: |
| # This odd little piece of code does the following: |
| # * add a bogus dictionary format %(foo)s to v. This forces % |
| # into 'dictionary lookup' mode |
| # * format the result with a defaultdict. This allows all |
| # `%(key)s` format lookups to succeed, but any sequential `%s` |
| # lookups to fail. |
| # If the string contains any accidental sequential lookups, this |
| # will raise an exception. If not, then this is a pluasible format |
| # string. |
| ('%(foo)s'+v) % collections.defaultdict(str) |
| except Exception: |
| raise ValueError(('Invalid %%-formatting parameter in envvar, ' |
| 'only %%(ENVVAR)s allowed: %r') % (v,)) |
| new[k] = v |
| _push(self._env, new) |
| |
| try: |
| yield |
| finally: |
| for p in to_pop: |
| p.pop() |
| |
| @property |
| def cwd(self): |
| """Returns the current working directory that steps will run in. |
| |
| **Returns (Path|None)** - The current working directory. A value of None is |
| equivalent to api.path['start_dir'], though only occurs if no cwd has been |
| set (e.g. in the outermost context of RunSteps). |
| """ |
| return self._cwd[-1] |
| |
| @property |
| def env(self): |
| """Returns modifications to the environment. |
| |
| By default this is empty; There's no facility to observe the program's |
| startup environment. If you want to pass data to the recipe, it should be |
| done with properties. |
| |
| **Returns (dict)** - The env-key -> value mapping of current environment |
| modifications. |
| """ |
| # TODO(iannucci): store env in an immutable way to avoid excessive copies. |
| # TODO(iannucci): handle case-insensitive keys on windows |
| return dict(self._env[-1]) |
| |
| @property |
| def env_prefixes(self): |
| """Returns Path prefix modifications to the environment. |
| |
| This will return a mapping of environment key to Path tuple for Path |
| prefixes registered with the environment. |
| |
| **Returns (dict)** - The env-key -> value(Path) mapping of current |
| environment prefix modifications. |
| """ |
| # TODO(iannucci): store env in an immutable way to avoid excessive copies. |
| # TODO(iannucci): handle case-insensitive keys on windows |
| return dict(self._env_prefixes[-1]) |
| |
| @property |
| def env_suffixes(self): |
| """Returns Path suffix modifications to the environment. |
| |
| This will return a mapping of environment key to Path tuple for Path |
| suffixes registered with the environment. |
| |
| **Returns (dict)** - The env-key -> value(Path) mapping of current |
| environment suffix modifications. |
| """ |
| # TODO(iannucci): store env in an immutable way to avoid excessive copies. |
| # TODO(iannucci): handle case-insensitive keys on windows |
| return dict(self._env_suffixes[-1]) |
| |
| @property |
| def infra_step(self): |
| """Returns the current value of the infra_step setting. |
| |
| **Returns (bool)** - True iff steps are currently considered infra steps. |
| """ |
| return self._infra_step[-1] |
| |
| @property |
| def name_prefix(self): |
| """Gets the current step name prefix. |
| |
| **Returns (str)** - The string prefix that every step will have prepended to |
| it. |
| """ |
| return self._name_prefix[-1] |
| |
| @property |
| def nest_level(self): |
| """Returns the current 'nesting' level. |
| |
| Note: This api is low-level, and you should always prefer to use |
| `api.step.nest`. This api is included for completeness and documentation |
| purposes. |
| |
| **Returns (int)** - The current nesting level. |
| """ |
| return self._nest_level[-1] |