Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(398)

Side by Side Diff: recipe_modules/context/api.py

Issue 2933473002: [context] Split "env" values and prefixes. (Closed)
Patch Set: fix typo Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « recipe_engine/unittests/test_env.py ('k') | recipe_modules/context/examples/full.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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)
OLDNEW
« no previous file with comments | « recipe_engine/unittests/test_env.py ('k') | recipe_modules/context/examples/full.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698