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

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

Issue 2925453002: [context] Add path prefix/suffix manipulation. (Closed)
Patch Set: comments 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
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.
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
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
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
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)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698